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
48fn resolve_tl_config_connection(name: &str) -> String {
52 if name.contains('=') || name.contains("://") {
54 return name.to_string();
55 }
56 let config_path =
58 std::env::var("TL_CONFIG_PATH").unwrap_or_else(|_| "tl_config.json".to_string());
59 let Ok(contents) = std::fs::read_to_string(&config_path) else {
60 return name.to_string();
61 };
62 let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
63 return name.to_string();
64 };
65 if let Some(conn) = json
67 .get("connections")
68 .and_then(|c| c.get(name))
69 .and_then(|v| v.as_str())
70 {
71 return conn.to_string();
72 }
73 if let Some(conn) = json.get(name).and_then(|v| v.as_str()) {
74 return conn.to_string();
75 }
76 name.to_string()
78}
79
80fn vm_values_equal(a: &VmValue, b: &VmValue) -> bool {
82 match (a, b) {
83 (VmValue::Int(x), VmValue::Int(y)) => x == y,
84 (VmValue::Float(x), VmValue::Float(y)) => x == y,
85 (VmValue::String(x), VmValue::String(y)) => x == y,
86 (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
87 (VmValue::None, VmValue::None) => true,
88 _ => false,
89 }
90}
91
92#[cfg(feature = "native")]
93fn resolve_package_file(pkg_root: &std::path::Path, remaining: &[&str]) -> Option<String> {
98 if remaining.is_empty() {
99 let src = pkg_root.join("src");
101 for entry in &["lib.tl", "mod.tl", "main.tl"] {
102 let p = src.join(entry);
103 if p.exists() {
104 return Some(p.to_string_lossy().to_string());
105 }
106 }
107 for entry in &["mod.tl", "lib.tl"] {
108 let p = pkg_root.join(entry);
109 if p.exists() {
110 return Some(p.to_string_lossy().to_string());
111 }
112 }
113 return None;
114 }
115
116 let rel = remaining.join("/");
118 let src = pkg_root.join("src");
119
120 let file_path = src.join(format!("{rel}.tl"));
121 if file_path.exists() {
122 return Some(file_path.to_string_lossy().to_string());
123 }
124
125 let dir_path = src.join(&rel).join("mod.tl");
126 if dir_path.exists() {
127 return Some(dir_path.to_string_lossy().to_string());
128 }
129
130 let file_path = pkg_root.join(format!("{rel}.tl"));
132 if file_path.exists() {
133 return Some(file_path.to_string_lossy().to_string());
134 }
135
136 let dir_path = pkg_root.join(&rel).join("mod.tl");
137 if dir_path.exists() {
138 return Some(dir_path.to_string_lossy().to_string());
139 }
140
141 if remaining.len() > 1 {
143 let parent = &remaining[..remaining.len() - 1];
144 let parent_rel = parent.join("/");
145 let parent_file = src.join(format!("{parent_rel}.tl"));
146 if parent_file.exists() {
147 return Some(parent_file.to_string_lossy().to_string());
148 }
149 let parent_file = pkg_root.join(format!("{parent_rel}.tl"));
150 if parent_file.exists() {
151 return Some(parent_file.to_string_lossy().to_string());
152 }
153 }
154
155 None
156}
157
158fn vm_json_to_value(v: &serde_json::Value) -> VmValue {
160 match v {
161 serde_json::Value::Null => VmValue::None,
162 serde_json::Value::Bool(b) => VmValue::Bool(*b),
163 serde_json::Value::Number(n) => {
164 if let Some(i) = n.as_i64() {
165 VmValue::Int(i)
166 } else {
167 VmValue::Float(n.as_f64().unwrap_or(0.0))
168 }
169 }
170 serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
171 serde_json::Value::Array(arr) => {
172 VmValue::List(Box::new(arr.iter().map(vm_json_to_value).collect()))
173 }
174 serde_json::Value::Object(obj) => VmValue::Map(Box::new(
175 obj.iter()
176 .map(|(k, v)| (Arc::from(k.as_str()), vm_json_to_value(v)))
177 .collect(),
178 )),
179 }
180}
181
182fn vm_value_to_json(v: &VmValue) -> serde_json::Value {
184 match v {
185 VmValue::None => serde_json::Value::Null,
186 VmValue::Bool(b) => serde_json::Value::Bool(*b),
187 VmValue::Int(n) => serde_json::json!(*n),
188 VmValue::Float(n) => serde_json::json!(*n),
189 VmValue::String(s) => serde_json::Value::String(s.to_string()),
190 VmValue::List(items) => {
191 serde_json::Value::Array(items.iter().map(vm_value_to_json).collect())
192 }
193 VmValue::Map(pairs) => {
194 let obj: serde_json::Map<String, serde_json::Value> = pairs
195 .iter()
196 .map(|(k, v)| (k.to_string(), vm_value_to_json(v)))
197 .collect();
198 serde_json::Value::Object(obj)
199 }
200 VmValue::Secret(_) => serde_json::Value::String("***".to_string()),
201 _ => serde_json::Value::String(format!("{v}")),
202 }
203}
204
205#[cfg(feature = "native")]
207const PARALLEL_THRESHOLD: usize = 10_000;
208
209#[cfg(feature = "native")]
211fn is_pure_closure(func: &VmValue) -> bool {
212 match func {
213 VmValue::Function(closure) => closure.upvalues.is_empty(),
214 _ => false,
215 }
216}
217
218#[cfg(feature = "native")]
221fn execute_pure_fn(proto: &Arc<Prototype>, args: &[VmValue]) -> Result<VmValue, TlError> {
222 let base = 0;
223 let num_regs = proto.num_registers as usize;
224 let mut stack = vec![VmValue::None; num_regs + 1];
225 for (i, arg) in args.iter().enumerate() {
226 stack[i] = arg.clone();
227 }
228
229 let mut ip = 0;
230 loop {
231 if ip >= proto.code.len() {
232 return Ok(VmValue::None);
233 }
234 let inst = proto.code[ip];
235 let op = decode_op(inst);
236 let a = decode_a(inst);
237 let b = decode_b(inst);
238 let c = decode_c(inst);
239 let bx = decode_bx(inst);
240 let sbx = decode_sbx(inst);
241
242 ip += 1;
243
244 match op {
245 Op::LoadConst => {
246 let val = match &proto.constants[bx as usize] {
247 Constant::Int(n) => VmValue::Int(*n),
248 Constant::Float(n) => VmValue::Float(*n),
249 Constant::String(s) => VmValue::String(s.clone()),
250 Constant::Decimal(s) => {
251 use std::str::FromStr;
252 VmValue::Decimal(rust_decimal::Decimal::from_str(s).unwrap_or_default())
253 }
254 _ => VmValue::None,
255 };
256 stack[base + a as usize] = val;
257 }
258 Op::LoadNone => stack[base + a as usize] = VmValue::None,
259 Op::LoadTrue => stack[base + a as usize] = VmValue::Bool(true),
260 Op::LoadFalse => stack[base + a as usize] = VmValue::Bool(false),
261 Op::Move | Op::GetLocal => {
262 let val = stack[base + b as usize].clone();
263 stack[base + a as usize] = val;
264 }
265 Op::SetLocal => {
266 let val = stack[base + a as usize].clone();
267 stack[base + b as usize] = val;
268 }
269 Op::Add => {
270 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
271 (VmValue::Int(x), VmValue::Int(y)) => x
272 .checked_add(*y)
273 .map(VmValue::Int)
274 .unwrap_or_else(|| VmValue::Float(*x as f64 + *y as f64)),
275 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x + y),
276 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 + y),
277 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x + *y as f64),
278 _ => return Err(runtime_err("Cannot add in parallel fn")),
279 };
280 stack[base + a as usize] = result;
281 }
282 Op::Sub => {
283 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
284 (VmValue::Int(x), VmValue::Int(y)) => x
285 .checked_sub(*y)
286 .map(VmValue::Int)
287 .unwrap_or_else(|| VmValue::Float(*x as f64 - *y as f64)),
288 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x - y),
289 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 - y),
290 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x - *y as f64),
291 _ => return Err(runtime_err("Cannot subtract in parallel fn")),
292 };
293 stack[base + a as usize] = result;
294 }
295 Op::Mul => {
296 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
297 (VmValue::Int(x), VmValue::Int(y)) => x
298 .checked_mul(*y)
299 .map(VmValue::Int)
300 .unwrap_or_else(|| VmValue::Float(*x as f64 * *y as f64)),
301 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x * y),
302 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 * y),
303 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x * *y as f64),
304 _ => return Err(runtime_err("Cannot multiply in parallel fn")),
305 };
306 stack[base + a as usize] = result;
307 }
308 Op::Div => {
309 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
310 (VmValue::Int(x), VmValue::Int(y)) => {
311 if *y == 0 {
312 return Err(runtime_err("Division by zero"));
313 }
314 VmValue::Int(x / y)
315 }
316 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x / y),
317 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 / y),
318 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x / *y as f64),
319 _ => return Err(runtime_err("Cannot divide in parallel fn")),
320 };
321 stack[base + a as usize] = result;
322 }
323 Op::Mod => {
324 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
325 (VmValue::Int(x), VmValue::Int(y)) => {
326 if *y == 0 {
327 return Err(runtime_err("Modulo by zero"));
328 }
329 VmValue::Int(x % y)
330 }
331 (VmValue::Float(x), VmValue::Float(y)) => {
332 if *y == 0.0 {
333 return Err(runtime_err("Modulo by zero"));
334 }
335 VmValue::Float(x % y)
336 }
337 _ => return Err(runtime_err("Cannot modulo in parallel fn")),
338 };
339 stack[base + a as usize] = result;
340 }
341 Op::Pow => {
342 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
343 (VmValue::Int(x), VmValue::Int(y)) => {
344 VmValue::Int((*x as f64).powi(*y as i32) as i64)
345 }
346 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x.powf(*y)),
347 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float((*x as f64).powf(*y)),
348 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x.powi(*y as i32)),
349 _ => return Err(runtime_err("Cannot pow in parallel fn")),
350 };
351 stack[base + a as usize] = result;
352 }
353 Op::Neg => {
354 let result = match &stack[base + b as usize] {
355 VmValue::Int(n) => VmValue::Int(-n),
356 VmValue::Float(n) => VmValue::Float(-n),
357 _ => return Err(runtime_err("Cannot negate in parallel fn")),
358 };
359 stack[base + a as usize] = result;
360 }
361 Op::Eq => {
362 let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
363 (VmValue::Int(x), VmValue::Int(y)) => x == y,
364 (VmValue::Float(x), VmValue::Float(y)) => x == y,
365 (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
366 (VmValue::String(x), VmValue::String(y)) => x == y,
367 (VmValue::None, VmValue::None) => true,
368 _ => false,
369 };
370 stack[base + a as usize] = VmValue::Bool(eq);
371 }
372 Op::Neq => {
373 let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
374 (VmValue::Int(x), VmValue::Int(y)) => x == y,
375 (VmValue::Float(x), VmValue::Float(y)) => x == y,
376 (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
377 (VmValue::String(x), VmValue::String(y)) => x == y,
378 (VmValue::None, VmValue::None) => true,
379 _ => false,
380 };
381 stack[base + a as usize] = VmValue::Bool(!eq);
382 }
383 Op::Lt | Op::Gt | Op::Lte | Op::Gte => {
384 let cmp = match (&stack[base + b as usize], &stack[base + c as usize]) {
385 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y) as i8,
386 (VmValue::Float(x), VmValue::Float(y)) => {
387 if x < y {
388 -1
389 } else if x > y {
390 1
391 } else {
392 0
393 }
394 }
395 _ => return Err(runtime_err("Cannot compare in parallel fn")),
396 };
397 let result = match op {
398 Op::Lt => cmp < 0,
399 Op::Gt => cmp > 0,
400 Op::Lte => cmp <= 0,
401 Op::Gte => cmp >= 0,
402 _ => unreachable!(),
403 };
404 stack[base + a as usize] = VmValue::Bool(result);
405 }
406 Op::And => {
407 let left = stack[base + b as usize].is_truthy();
408 let right = stack[base + c as usize].is_truthy();
409 stack[base + a as usize] = VmValue::Bool(left && right);
410 }
411 Op::Or => {
412 let left = stack[base + b as usize].is_truthy();
413 let right = stack[base + c as usize].is_truthy();
414 stack[base + a as usize] = VmValue::Bool(left || right);
415 }
416 Op::Not => {
417 let val = !stack[base + b as usize].is_truthy();
418 stack[base + a as usize] = VmValue::Bool(val);
419 }
420 Op::Jump => {
421 ip = (ip as i32 + sbx as i32) as usize;
422 }
423 Op::JumpIfFalse => {
424 if !stack[base + a as usize].is_truthy() {
425 ip = (ip as i32 + sbx as i32) as usize;
426 }
427 }
428 Op::JumpIfTrue => {
429 if stack[base + a as usize].is_truthy() {
430 ip = (ip as i32 + sbx as i32) as usize;
431 }
432 }
433 Op::Return => {
434 return Ok(stack[base + a as usize].clone());
435 }
436 _ => return Err(runtime_err("Unsupported op in parallel function")),
438 }
439 }
440}
441
442struct CallFrame {
444 prototype: Arc<Prototype>,
445 ip: usize,
446 base: usize,
447 upvalues: Vec<UpvalueRef>,
448}
449
450struct TryHandler {
452 frame_idx: usize,
454 catch_ip: usize,
456}
457
458pub struct Vm {
460 pub stack: Vec<VmValue>,
462 frames: Vec<CallFrame>,
464 pub globals: HashMap<String, VmValue>,
466 #[cfg(feature = "native")]
468 data_engine: Option<DataEngine>,
469 pub output: Vec<String>,
471 try_handlers: Vec<TryHandler>,
473 yielded_value: Option<VmValue>,
475 yielded_ip: usize,
477 pub file_path: Option<String>,
479 module_cache: HashMap<String, HashMap<String, VmValue>>,
481 importing_files: std::collections::HashSet<String>,
483 pub public_items: std::collections::HashSet<String>,
485 pub package_roots: HashMap<String, std::path::PathBuf>,
487 pub project_root: Option<std::path::PathBuf>,
489 pub schema_registry: crate::schema::SchemaRegistry,
491 pub secret_vault: SecretVault,
493 pub security_policy: Option<crate::security::SecurityPolicy>,
495 #[cfg(feature = "async-runtime")]
497 runtime: Option<Arc<tokio::runtime::Runtime>>,
498 thrown_value: Option<VmValue>,
500 #[cfg(feature = "gpu")]
502 gpu_ops: Option<tl_gpu::GpuOps>,
503 #[cfg(feature = "mcp")]
505 mcp_agent_clients: HashMap<String, Vec<Arc<tl_mcp::McpClient>>>,
506}
507
508#[derive(Debug, Clone, Default)]
510pub struct SecretVault(HashMap<String, String>);
511
512impl SecretVault {
513 pub fn new() -> Self {
514 Self(HashMap::new())
515 }
516 pub fn get(&self, key: &str) -> Option<&String> {
517 self.0.get(key)
518 }
519 pub fn insert(&mut self, key: String, val: String) {
520 self.0.insert(key, val);
521 }
522 pub fn remove(&mut self, key: &str) {
523 self.0.remove(key);
524 }
525 pub fn keys(&self) -> impl Iterator<Item = &String> {
526 self.0.keys()
527 }
528}
529
530impl Drop for SecretVault {
531 fn drop(&mut self) {
532 for val in self.0.values_mut() {
533 unsafe {
536 let ptr = val.as_mut_vec().as_mut_ptr();
537 std::ptr::write_bytes(ptr, 0, val.len());
538 }
539 }
540 self.0.clear();
541 }
542}
543
544impl Vm {
545 pub fn new() -> Self {
546 let mut vm = Vm {
547 stack: Vec::with_capacity(256),
548 frames: Vec::new(),
549 globals: HashMap::new(),
550 #[cfg(feature = "native")]
551 data_engine: None,
552 output: Vec::new(),
553 try_handlers: Vec::new(),
554 yielded_value: None,
555 yielded_ip: 0,
556 file_path: None,
557 module_cache: HashMap::new(),
558 importing_files: std::collections::HashSet::new(),
559 public_items: std::collections::HashSet::new(),
560 package_roots: HashMap::new(),
561 project_root: None,
562 schema_registry: crate::schema::SchemaRegistry::new(),
563 secret_vault: SecretVault::new(),
564 security_policy: None,
565 #[cfg(feature = "async-runtime")]
566 runtime: None,
567 thrown_value: None,
568 #[cfg(feature = "gpu")]
569 gpu_ops: None,
570 #[cfg(feature = "mcp")]
571 mcp_agent_clients: HashMap::new(),
572 };
573 vm.globals.insert(
575 "DataError".into(),
576 VmValue::EnumDef(Arc::new(VmEnumDef {
577 name: Arc::from("DataError"),
578 variants: vec![
579 (Arc::from("ParseError"), 2),
580 (Arc::from("SchemaError"), 3),
581 (Arc::from("ValidationError"), 2),
582 (Arc::from("NotFound"), 1),
583 ],
584 })),
585 );
586 vm.globals.insert(
587 "NetworkError".into(),
588 VmValue::EnumDef(Arc::new(VmEnumDef {
589 name: Arc::from("NetworkError"),
590 variants: vec![
591 (Arc::from("ConnectionError"), 2),
592 (Arc::from("TimeoutError"), 1),
593 (Arc::from("HttpError"), 2),
594 ],
595 })),
596 );
597 vm.globals.insert(
598 "ConnectorError".into(),
599 VmValue::EnumDef(Arc::new(VmEnumDef {
600 name: Arc::from("ConnectorError"),
601 variants: vec![
602 (Arc::from("AuthError"), 2),
603 (Arc::from("QueryError"), 2),
604 (Arc::from("ConfigError"), 2),
605 ],
606 })),
607 );
608 #[cfg(feature = "mcp")]
610 {
611 vm.globals.insert(
612 "mcp_connect".to_string(),
613 VmValue::Builtin(BuiltinId::McpConnect),
614 );
615 vm.globals.insert(
616 "mcp_list_tools".to_string(),
617 VmValue::Builtin(BuiltinId::McpListTools),
618 );
619 vm.globals.insert(
620 "mcp_call_tool".to_string(),
621 VmValue::Builtin(BuiltinId::McpCallTool),
622 );
623 vm.globals.insert(
624 "mcp_disconnect".to_string(),
625 VmValue::Builtin(BuiltinId::McpDisconnect),
626 );
627 vm.globals.insert(
628 "mcp_serve".to_string(),
629 VmValue::Builtin(BuiltinId::McpServe),
630 );
631 vm.globals.insert(
632 "mcp_server_info".to_string(),
633 VmValue::Builtin(BuiltinId::McpServerInfo),
634 );
635 vm.globals
636 .insert("mcp_ping".to_string(), VmValue::Builtin(BuiltinId::McpPing));
637 vm.globals.insert(
638 "mcp_list_resources".to_string(),
639 VmValue::Builtin(BuiltinId::McpListResources),
640 );
641 vm.globals.insert(
642 "mcp_read_resource".to_string(),
643 VmValue::Builtin(BuiltinId::McpReadResource),
644 );
645 vm.globals.insert(
646 "mcp_list_prompts".to_string(),
647 VmValue::Builtin(BuiltinId::McpListPrompts),
648 );
649 vm.globals.insert(
650 "mcp_get_prompt".to_string(),
651 VmValue::Builtin(BuiltinId::McpGetPrompt),
652 );
653 }
654 vm
655 }
656
657 #[cfg(feature = "async-runtime")]
659 fn ensure_runtime(&mut self) -> Arc<tokio::runtime::Runtime> {
660 if self.runtime.is_none() {
661 self.runtime = Some(Arc::new(
662 tokio::runtime::Builder::new_multi_thread()
663 .enable_all()
664 .build()
665 .expect("Failed to create tokio runtime"),
666 ));
667 }
668 self.runtime.as_ref().unwrap().clone()
669 }
670
671 #[cfg(feature = "gpu")]
673 fn get_gpu_ops(&mut self) -> Result<&tl_gpu::GpuOps, TlError> {
674 if self.gpu_ops.is_none() {
675 let device =
676 tl_gpu::GpuDevice::get().ok_or_else(|| runtime_err("No GPU device available"))?;
677 self.gpu_ops = Some(tl_gpu::GpuOps::new(device));
678 }
679 Ok(self.gpu_ops.as_ref().unwrap())
680 }
681
682 #[cfg(feature = "gpu")]
684 fn ensure_gpu_tensor(&mut self, val: &VmValue) -> Result<Arc<tl_gpu::GpuTensor>, TlError> {
685 match val {
686 VmValue::GpuTensor(gt) => Ok(gt.clone()),
687 #[cfg(feature = "native")]
688 VmValue::Tensor(t) => {
689 let device = tl_gpu::GpuDevice::get()
690 .ok_or_else(|| runtime_err("No GPU device available"))?;
691 Ok(Arc::new(tl_gpu::GpuTensor::from_cpu(t, device)))
692 }
693 _ => Err(runtime_err(format!(
694 "Expected tensor or gpu_tensor, got {}",
695 val.type_name()
696 ))),
697 }
698 }
699
700 #[cfg(feature = "native")]
701 fn engine(&mut self) -> &DataEngine {
702 if self.data_engine.is_none() {
703 self.data_engine = Some(DataEngine::new());
704 }
705 self.data_engine.as_ref().unwrap()
706 }
707
708 fn ensure_stack(&mut self, size: usize) {
710 if self.stack.len() < size {
711 self.stack.resize(size, VmValue::None);
712 }
713 }
714
715 pub fn execute(&mut self, proto: &Prototype) -> Result<VmValue, TlError> {
717 let proto = Arc::new(proto.clone());
718 let base = self.stack.len();
719 self.ensure_stack(base + proto.num_registers as usize + 1);
720
721 self.frames.push(CallFrame {
722 prototype: proto,
723 ip: 0,
724 base,
725 upvalues: Vec::new(),
726 });
727
728 self.run().map_err(|e| self.enrich_error(e))
729 }
730
731 pub fn debug_load(&mut self, proto: &Prototype) {
735 let proto = Arc::new(proto.clone());
736 let base = self.stack.len();
737 self.ensure_stack(base + proto.num_registers as usize + 1);
738 self.frames.push(CallFrame {
739 prototype: proto,
740 ip: 0,
741 base,
742 upvalues: Vec::new(),
743 });
744 }
745
746 pub fn debug_step(&mut self) -> Result<Option<VmValue>, TlError> {
751 let entry_depth = 1; self.run_step(entry_depth).map_err(|e| self.enrich_error(e))
753 }
754
755 pub fn debug_current_line(&self) -> u32 {
757 if let Some(frame) = self.frames.last() {
758 let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
759 if ip < frame.prototype.lines.len() {
760 frame.prototype.lines[ip]
761 } else {
762 0
763 }
764 } else {
765 0
766 }
767 }
768
769 pub fn debug_current_function(&self) -> String {
771 self.frames
772 .last()
773 .map(|f| f.prototype.name.clone())
774 .unwrap_or_default()
775 }
776
777 pub fn debug_is_done(&self) -> bool {
779 self.frames.is_empty()
780 || self
781 .frames
782 .last()
783 .is_some_and(|f| f.ip >= f.prototype.code.len())
784 }
785
786 pub fn debug_get_global(&self, name: &str) -> Option<&VmValue> {
788 self.globals.get(name)
789 }
790
791 pub fn debug_get_local(&self, name: &str) -> Option<&VmValue> {
793 if let Some(frame) = self.frames.last() {
794 for (local_name, reg) in &frame.prototype.top_level_locals {
795 if local_name == name {
796 let idx = frame.base + *reg as usize;
797 if idx < self.stack.len() {
798 return Some(&self.stack[idx]);
799 }
800 }
801 }
802 }
803 None
804 }
805
806 pub fn debug_locals(&self) -> Vec<(String, &VmValue)> {
808 let mut result = Vec::new();
809 if let Some(frame) = self.frames.last() {
810 for (name, reg) in &frame.prototype.top_level_locals {
811 let idx = frame.base + *reg as usize;
812 if idx < self.stack.len() {
813 result.push((name.clone(), &self.stack[idx]));
814 }
815 }
816 }
817 result
818 }
819
820 pub fn debug_current_ip(&self) -> usize {
822 self.frames.last().map(|f| f.ip).unwrap_or(0)
823 }
824
825 pub fn debug_step_line(&mut self) -> Result<Option<VmValue>, TlError> {
827 let start_line = self.debug_current_line();
828 loop {
829 if self.debug_is_done() {
830 return Ok(Some(VmValue::None));
831 }
832 let result = self.debug_step()?;
833 if result.is_some() {
834 return Ok(result);
835 }
836 let new_line = self.debug_current_line();
837 if new_line != start_line && new_line != 0 {
838 return Ok(None);
839 }
840 }
841 }
842
843 pub fn debug_continue(&mut self, breakpoints: &[u32]) -> Result<Option<VmValue>, TlError> {
845 loop {
846 if self.debug_is_done() {
847 return Ok(Some(VmValue::None));
848 }
849 let result = self.debug_step()?;
850 if result.is_some() {
851 return Ok(result);
852 }
853 let line = self.debug_current_line();
854 if breakpoints.contains(&line) {
855 return Ok(None);
856 }
857 }
858 }
859
860 fn enrich_error(&self, err: TlError) -> TlError {
862 match err {
863 TlError::Runtime(mut re) => {
864 let mut trace = Vec::new();
866 for frame in self.frames.iter().rev() {
867 let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
868 let line = if ip < frame.prototype.lines.len() {
869 frame.prototype.lines[ip]
870 } else {
871 0
872 };
873 trace.push(tl_errors::StackFrame {
874 function: frame.prototype.name.clone(),
875 line,
876 });
877 }
878 if re.span.is_none() && !trace.is_empty() && trace[0].line > 0 {
880 }
884 re.stack_trace = trace;
885 TlError::Runtime(re)
886 }
887 other => other,
888 }
889 }
890
891 fn run(&mut self) -> Result<VmValue, TlError> {
893 let entry_depth = self.frames.len();
894 loop {
895 let step_result = self.run_step(entry_depth);
896 match step_result {
897 Ok(Some(val)) => return Ok(val), Ok(None) => continue, Err(e) => {
900 if let Some(handler) = self.try_handlers.pop() {
902 while self.frames.len() > handler.frame_idx {
904 self.frames.pop();
905 }
906 if self.frames.is_empty() {
907 return Err(e);
908 }
909 let fidx = self.frames.len() - 1;
910 self.frames[fidx].ip = handler.catch_ip;
911 let err_msg = match &e {
912 TlError::Runtime(re) => re.message.clone(),
913 other => format!("{other}"),
914 };
915 let catch_val = self
919 .thrown_value
920 .take()
921 .unwrap_or_else(|| VmValue::String(Arc::from(err_msg.as_str())));
922 let cbase = self.frames[fidx].base;
923 let current_ip = self.frames[fidx].ip;
924 if current_ip < self.frames[fidx].prototype.code.len() {
925 let catch_inst = self.frames[fidx].prototype.code[current_ip];
926 let catch_op = decode_op(catch_inst);
927 let catch_reg = decode_a(catch_inst);
928 if matches!(catch_op, Op::LoadNone) {
929 self.frames[fidx].ip += 1;
931 self.ensure_stack(cbase + catch_reg as usize + 1);
932 self.stack[cbase + catch_reg as usize] = catch_val;
933 }
934 }
935 continue;
936 }
937 return Err(e);
938 }
939 }
940 }
941 }
942
943 fn run_step(&mut self, entry_depth: usize) -> Result<Option<VmValue>, TlError> {
945 if self.frames.len() < entry_depth || self.frames.is_empty() {
946 return Ok(Some(VmValue::None));
947 }
948 let frame_idx = self.frames.len() - 1;
949 let frame = &self.frames[frame_idx];
950
951 if frame.ip >= frame.prototype.code.len() {
952 self.frames.pop();
954 return Ok(Some(VmValue::None));
955 }
956
957 let inst = frame.prototype.code[frame.ip];
958 let op = decode_op(inst);
959 let a = decode_a(inst);
960 let b = decode_b(inst);
961 let c = decode_c(inst);
962 let bx = decode_bx(inst);
963 let sbx = decode_sbx(inst);
964 let base = frame.base;
965
966 self.frames[frame_idx].ip += 1;
968
969 match op {
970 Op::LoadConst => {
971 let val = self.load_constant(frame_idx, bx)?;
972 self.stack[base + a as usize] = val;
973 }
974 Op::LoadNone => {
975 self.stack[base + a as usize] = VmValue::None;
976 }
977 Op::LoadTrue => {
978 self.stack[base + a as usize] = VmValue::Bool(true);
979 }
980 Op::LoadFalse => {
981 self.stack[base + a as usize] = VmValue::Bool(false);
982 }
983 Op::Move => {
984 let val = &self.stack[base + b as usize];
985 if matches!(val, VmValue::Moved) {
986 return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
987 }
988 self.stack[base + a as usize] = val.clone();
989 }
990 Op::GetLocal => {
991 let val = &self.stack[base + b as usize];
992 if matches!(val, VmValue::Moved) {
993 return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
994 }
995 self.stack[base + a as usize] = val.clone();
996 }
997 Op::SetLocal => {
998 let val = self.stack[base + a as usize].clone();
999 self.stack[base + b as usize] = val;
1000 }
1001 Op::GetGlobal => {
1002 let name = self.get_string_constant(frame_idx, bx)?;
1003 let val = self
1004 .globals
1005 .get(name.as_ref())
1006 .cloned()
1007 .unwrap_or(VmValue::None);
1008 if matches!(val, VmValue::Moved) {
1009 return Err(runtime_err(format!(
1010 "Use of moved value `{name}`. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy."
1011 )));
1012 }
1013 self.stack[base + a as usize] = val;
1014 }
1015 Op::SetGlobal => {
1016 let name = self.get_string_constant(frame_idx, bx)?;
1017 let val = self.stack[base + a as usize].clone();
1018 #[cfg(feature = "native")]
1020 if let VmValue::String(ref s) = val {
1021 if s.starts_with("__schema__:") {
1022 self.process_schema_global(s);
1023 } else if s.starts_with("__migrate__:") {
1024 self.process_migrate_global(s);
1025 }
1026 }
1027 self.globals.insert(name.to_string(), val);
1028 }
1029 Op::GetUpvalue => {
1030 let val = {
1031 let frame = &self.frames[frame_idx];
1032 match &frame.upvalues[b as usize] {
1033 UpvalueRef::Open { stack_index } => self.stack[*stack_index].clone(),
1034 UpvalueRef::Closed(v) => v.clone(),
1035 }
1036 };
1037 self.stack[base + a as usize] = val;
1038 }
1039 Op::SetUpvalue => {
1040 let val = self.stack[base + a as usize].clone();
1041 let frame = &mut self.frames[frame_idx];
1042 match &mut frame.upvalues[b as usize] {
1043 UpvalueRef::Open { stack_index } => {
1044 let idx = *stack_index;
1045 self.stack[idx] = val;
1046 }
1047 UpvalueRef::Closed(v) => {
1048 *v = val;
1049 }
1050 }
1051 }
1052 Op::Add => {
1053 let result = self.vm_add(base, b, c)?;
1054 self.stack[base + a as usize] = result;
1055 }
1056 Op::Sub => {
1057 let result = self.vm_sub(base, b, c)?;
1058 self.stack[base + a as usize] = result;
1059 }
1060 Op::Mul => {
1061 let result = self.vm_mul(base, b, c)?;
1062 self.stack[base + a as usize] = result;
1063 }
1064 Op::Div => {
1065 let result = self.vm_div(base, b, c)?;
1066 self.stack[base + a as usize] = result;
1067 }
1068 Op::Mod => {
1069 let result = self.vm_mod(base, b, c)?;
1070 self.stack[base + a as usize] = result;
1071 }
1072 Op::Pow => {
1073 let result = self.vm_pow(base, b, c)?;
1074 self.stack[base + a as usize] = result;
1075 }
1076 Op::Neg => {
1077 let result = match &self.stack[base + b as usize] {
1078 VmValue::Int(n) => VmValue::Int(-n),
1079 VmValue::Float(n) => VmValue::Float(-n),
1080 VmValue::Decimal(d) => VmValue::Decimal(-d),
1081 other => {
1082 return Err(runtime_err(format!("Cannot negate {}", other.type_name())));
1083 }
1084 };
1085 self.stack[base + a as usize] = result;
1086 }
1087 Op::Eq => {
1088 let result = self.vm_eq(base, b, c);
1089 self.stack[base + a as usize] = VmValue::Bool(result);
1090 }
1091 Op::Neq => {
1092 let result = !self.vm_eq(base, b, c);
1093 self.stack[base + a as usize] = VmValue::Bool(result);
1094 }
1095 Op::Lt => {
1096 let result = self.vm_cmp(base, b, c)?;
1097 self.stack[base + a as usize] = VmValue::Bool(result == Some(-1));
1098 }
1099 Op::Gt => {
1100 let result = self.vm_cmp(base, b, c)?;
1101 self.stack[base + a as usize] = VmValue::Bool(result == Some(1));
1102 }
1103 Op::Lte => {
1104 let result = self.vm_cmp(base, b, c)?;
1105 self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(-1) | Some(0)));
1106 }
1107 Op::Gte => {
1108 let result = self.vm_cmp(base, b, c)?;
1109 self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(0) | Some(1)));
1110 }
1111 Op::And => {
1112 let left = self.stack[base + b as usize].is_truthy();
1113 let right = self.stack[base + c as usize].is_truthy();
1114 self.stack[base + a as usize] = VmValue::Bool(left && right);
1115 }
1116 Op::Or => {
1117 let left = self.stack[base + b as usize].is_truthy();
1118 let right = self.stack[base + c as usize].is_truthy();
1119 self.stack[base + a as usize] = VmValue::Bool(left || right);
1120 }
1121 Op::Not => {
1122 let val = !self.stack[base + b as usize].is_truthy();
1123 self.stack[base + a as usize] = VmValue::Bool(val);
1124 }
1125 Op::Concat => {
1126 let left = format!("{}", self.stack[base + b as usize]);
1127 let right = format!("{}", self.stack[base + c as usize]);
1128 self.stack[base + a as usize] =
1129 VmValue::String(Arc::from(format!("{left}{right}").as_str()));
1130 }
1131 Op::Jump => {
1132 let frame = &mut self.frames[frame_idx];
1133 frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1134 }
1135 Op::JumpIfFalse => {
1136 if !self.stack[base + a as usize].is_truthy() {
1137 let frame = &mut self.frames[frame_idx];
1138 frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1139 }
1140 }
1141 Op::JumpIfTrue => {
1142 if self.stack[base + a as usize].is_truthy() {
1143 let frame = &mut self.frames[frame_idx];
1144 frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1145 }
1146 }
1147 Op::Call => {
1148 let func_val = self.stack[base + a as usize].clone();
1150 self.do_call(func_val, base, a, b, c)?;
1151 }
1152 Op::Return => {
1153 let return_val = self.stack[base + a as usize].clone();
1154 self.frames.pop();
1155 return Ok(Some(return_val));
1156 }
1157 Op::Closure => {
1158 let proto = match &self.frames[frame_idx].prototype.constants[bx as usize] {
1159 Constant::Prototype(p) => p.clone(),
1160 _ => return Err(runtime_err("Expected prototype constant")),
1161 };
1162
1163 let mut upvalues = Vec::new();
1165 for def in &proto.upvalue_defs {
1166 if def.is_local {
1167 upvalues.push(UpvalueRef::Open {
1168 stack_index: base + def.index as usize,
1169 });
1170 } else {
1171 let frame = &self.frames[frame_idx];
1172 upvalues.push(frame.upvalues[def.index as usize].clone());
1173 }
1174 }
1175
1176 let closure = VmClosure {
1177 prototype: proto,
1178 upvalues,
1179 };
1180 self.stack[base + a as usize] = VmValue::Function(Arc::new(closure));
1181 }
1182 Op::NewList => {
1183 let mut items = Vec::with_capacity(c as usize);
1185 for i in 0..c as usize {
1186 items.push(self.stack[base + b as usize + i].clone());
1187 }
1188 self.stack[base + a as usize] = VmValue::List(Box::new(items));
1189 }
1190 Op::GetIndex => {
1191 let raw_obj = &self.stack[base + b as usize];
1192 let obj = match raw_obj {
1193 VmValue::Ref(inner) => inner.as_ref(),
1194 other => other,
1195 };
1196 let idx = &self.stack[base + c as usize];
1197 let result = match (obj, idx) {
1198 (VmValue::List(items), VmValue::Int(i)) => {
1199 let idx = if *i < 0 {
1200 let adjusted = items.len() as i64 + *i;
1201 if adjusted < 0 {
1202 return Err(runtime_err(format!(
1203 "Index {} out of bounds for list of length {}",
1204 i,
1205 items.len()
1206 )));
1207 }
1208 adjusted as usize
1209 } else {
1210 *i as usize
1211 };
1212 items.get(idx).cloned().ok_or_else(|| {
1213 runtime_err(format!(
1214 "Index {} out of bounds for list of length {}",
1215 i,
1216 items.len()
1217 ))
1218 })?
1219 }
1220 (VmValue::Map(pairs), VmValue::String(key)) => pairs
1221 .iter()
1222 .find(|(k, _)| k.as_ref() == key.as_ref())
1223 .map(|(_, v)| v.clone())
1224 .unwrap_or(VmValue::None),
1225 _ => {
1226 return Err(runtime_err(format!(
1227 "Cannot index {} with {}",
1228 obj.type_name(),
1229 idx.type_name()
1230 )));
1231 }
1232 };
1233 self.stack[base + a as usize] = result;
1234 }
1235 Op::SetIndex => {
1236 if matches!(&self.stack[base + b as usize], VmValue::Ref(_)) {
1237 return Err(runtime_err(
1238 "Cannot mutate a borrowed reference".to_string(),
1239 ));
1240 }
1241 let val = self.stack[base + a as usize].clone();
1242 let idx_val = self.stack[base + c as usize].clone();
1243 match idx_val {
1244 VmValue::Int(i) => {
1245 if let VmValue::List(ref mut items) = self.stack[base + b as usize] {
1246 let idx = if i < 0 {
1247 let adjusted = items.len() as i64 + i;
1248 if adjusted < 0 {
1249 return Err(runtime_err(format!(
1250 "Index {} out of bounds for list of length {}",
1251 i,
1252 items.len()
1253 )));
1254 }
1255 adjusted as usize
1256 } else {
1257 i as usize
1258 };
1259 if idx < items.len() {
1260 items[idx] = val;
1261 } else {
1262 return Err(runtime_err(format!(
1263 "Index {} out of bounds for list of length {}",
1264 i,
1265 items.len()
1266 )));
1267 }
1268 }
1269 }
1270 VmValue::String(key) => {
1271 if let VmValue::Map(ref mut pairs) = self.stack[base + b as usize] {
1272 if let Some(entry) =
1273 pairs.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref())
1274 {
1275 entry.1 = val;
1276 } else {
1277 pairs.push((key, val));
1278 }
1279 }
1280 }
1281 _ => {}
1282 }
1283 }
1284 Op::NewMap => {
1285 let mut pairs = Vec::with_capacity(c as usize);
1288 for i in 0..c as usize {
1289 let key_val = &self.stack[base + b as usize + i * 2];
1290 let val = self.stack[base + b as usize + i * 2 + 1].clone();
1291 let key = match key_val {
1292 VmValue::String(s) => s.clone(),
1293 other => Arc::from(format!("{other}").as_str()),
1294 };
1295 pairs.push((key, val));
1296 }
1297 self.stack[base + a as usize] = VmValue::Map(Box::new(pairs));
1298 }
1299 Op::TablePipe => {
1300 #[cfg(feature = "native")]
1301 {
1302 let table_val = self.stack[base + a as usize].clone();
1304 let result = self.handle_table_pipe(frame_idx, table_val, b, c)?;
1305 self.stack[base + a as usize] = result;
1306 }
1307 #[cfg(not(feature = "native"))]
1308 {
1309 let _ = (a, b, c, frame_idx);
1310 return Err(runtime_err("Table operations not available in WASM"));
1311 }
1312 }
1313 Op::CallBuiltin => {
1314 let builtin_id = decode_bx(inst);
1317 let next_inst = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1318 self.frames[frame_idx].ip += 1;
1319 let arg_count = decode_a(next_inst) as usize;
1320 let first_arg = decode_b(next_inst) as usize;
1321
1322 let result = self.call_builtin(builtin_id, base + first_arg, arg_count)?;
1323 self.stack[base + a as usize] = result;
1324 }
1325 Op::ForIter => {
1326 let idx = match &self.stack[base + a as usize] {
1328 VmValue::Int(i) => *i as usize,
1329 _ => 0,
1330 };
1331 let list = &self.stack[base + b as usize];
1332 let done = match list {
1333 VmValue::List(items) => {
1334 if idx < items.len() {
1335 let item = items[idx].clone();
1336 self.stack[base + c as usize] = item;
1337 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1338 false
1339 } else {
1340 true
1341 }
1342 }
1343 VmValue::Map(pairs) => {
1344 if idx < pairs.len() {
1345 let (k, v) = &pairs[idx];
1346 let pair = VmValue::List(Box::new(vec![
1347 VmValue::String(k.clone()),
1348 v.clone(),
1349 ]));
1350 self.stack[base + c as usize] = pair;
1351 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1352 false
1353 } else {
1354 true
1355 }
1356 }
1357 VmValue::Set(items) => {
1358 if idx < items.len() {
1359 let item = items[idx].clone();
1360 self.stack[base + c as usize] = item;
1361 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1362 false
1363 } else {
1364 true
1365 }
1366 }
1367 VmValue::Generator(gen_arc) => {
1368 let g = gen_arc.clone();
1369 let val = self.generator_next(&g)?;
1370 if matches!(val, VmValue::None) {
1371 true
1372 } else {
1373 self.stack[base + c as usize] = val;
1374 false
1375 }
1376 }
1377 _ => true,
1378 };
1379 if done {
1380 } else {
1383 self.frames[frame_idx].ip += 1;
1385 }
1386 }
1387 Op::ForPrep => {
1388 }
1390 Op::TestMatch => {
1391 let subject = &self.stack[base + a as usize];
1393 let pattern = &self.stack[base + b as usize];
1394 let matched = match (subject, pattern) {
1395 (VmValue::Int(a), VmValue::Int(b)) => a == b,
1396 (VmValue::Float(a), VmValue::Float(b)) => a == b,
1397 (VmValue::String(a), VmValue::String(b)) => a == b,
1398 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
1399 (VmValue::None, VmValue::None) => true,
1400 (VmValue::EnumInstance(subj), VmValue::EnumInstance(pat)) => {
1402 subj.type_name == pat.type_name && subj.variant == pat.variant
1403 }
1404 (VmValue::StructInstance(s), VmValue::String(name)) => {
1406 s.type_name.as_ref() == name.as_ref()
1407 }
1408 _ => false,
1409 };
1410 self.stack[base + c as usize] = VmValue::Bool(matched);
1411 }
1412 Op::NullCoalesce => {
1413 if matches!(self.stack[base + a as usize], VmValue::None) {
1414 let val = self.stack[base + b as usize].clone();
1415 self.stack[base + a as usize] = val;
1416 }
1417 }
1418 Op::GetMember => {
1419 let field_name = self.get_string_constant(frame_idx, c as u16)?;
1421 let raw_obj = self.stack[base + b as usize].clone();
1422 let obj = match &raw_obj {
1423 VmValue::Ref(inner) => inner.as_ref().clone(),
1424 _ => raw_obj,
1425 };
1426 let result = match &obj {
1427 VmValue::StructInstance(inst) => inst
1428 .fields
1429 .iter()
1430 .find(|(k, _)| k.as_ref() == field_name.as_ref())
1431 .map(|(_, v)| v.clone())
1432 .unwrap_or(VmValue::None),
1433 VmValue::Module(m) => m
1434 .exports
1435 .get(field_name.as_ref())
1436 .cloned()
1437 .unwrap_or(VmValue::None),
1438 VmValue::EnumInstance(e) => match field_name.as_ref() {
1439 "variant" => VmValue::String(e.variant.clone()),
1440 "type_name" => VmValue::String(e.type_name.clone()),
1441 _ => VmValue::None,
1442 },
1443 VmValue::Map(pairs) => pairs
1444 .iter()
1445 .find(|(k, _)| k.as_ref() == field_name.as_ref())
1446 .map(|(_, v)| v.clone())
1447 .unwrap_or(VmValue::None),
1448 #[cfg(feature = "python")]
1449 VmValue::PyObject(wrapper) => {
1450 crate::python::py_get_member(wrapper, field_name.as_ref())
1451 }
1452 _ => VmValue::None,
1453 };
1454 self.stack[base + a as usize] = result;
1455 }
1456 Op::Interpolate => {
1457 let template = self.get_string_constant(frame_idx, bx)?;
1459 let result = self.interpolate_string(&template, base)?;
1460 self.stack[base + a as usize] = VmValue::String(Arc::from(result.as_str()));
1461 }
1462 Op::Train => {
1463 #[cfg(feature = "native")]
1464 {
1465 let result = self.handle_train(frame_idx, b, c)?;
1466 self.stack[base + a as usize] = result;
1467 }
1468 #[cfg(not(feature = "native"))]
1469 {
1470 let _ = (a, b, c, frame_idx);
1471 return Err(runtime_err("AI training not available in WASM"));
1472 }
1473 }
1474 Op::PipelineExec => {
1475 #[cfg(feature = "native")]
1476 {
1477 let result = self.handle_pipeline_exec(frame_idx, b, c)?;
1478 self.stack[base + a as usize] = result;
1479 }
1480 #[cfg(not(feature = "native"))]
1481 {
1482 let _ = (a, b, c, frame_idx);
1483 return Err(runtime_err("Pipelines not available in WASM"));
1484 }
1485 }
1486 Op::StreamExec => {
1487 #[cfg(feature = "native")]
1488 {
1489 let result = self.handle_stream_exec(frame_idx, b)?;
1490 self.stack[base + a as usize] = result;
1491 }
1492 #[cfg(not(feature = "native"))]
1493 {
1494 let _ = (a, b, frame_idx);
1495 return Err(runtime_err("Streaming not available in WASM"));
1496 }
1497 }
1498 Op::ConnectorDecl => {
1499 #[cfg(feature = "native")]
1500 {
1501 let result = self.handle_connector_decl(frame_idx, b, c)?;
1502 self.stack[base + a as usize] = result;
1503 }
1504 #[cfg(not(feature = "native"))]
1505 {
1506 let _ = (a, b, c, frame_idx);
1507 return Err(runtime_err("Connectors not available in WASM"));
1508 }
1509 }
1510
1511 Op::NewStruct => {
1513 let name = self.get_string_constant(frame_idx, b as u16)?;
1520
1521 let is_decl = (c & 0x80) != 0;
1525
1526 if is_decl {
1527 let const_idx = (c & 0x7F) as usize;
1528 let fields_data = match &self.frames[frame_idx].prototype.constants[const_idx] {
1530 Constant::AstExprList(exprs) => exprs.clone(),
1531 _ => Vec::new(),
1532 };
1533 let is_enum = fields_data
1535 .first()
1536 .map(|e| {
1537 if let AstExpr::String(s) = e {
1538 s.contains(':')
1539 } else {
1540 false
1541 }
1542 })
1543 .unwrap_or(false);
1544
1545 if is_enum {
1546 let variants: Vec<(Arc<str>, usize)> = fields_data
1547 .iter()
1548 .filter_map(|e| {
1549 if let AstExpr::String(s) = e {
1550 let parts: Vec<&str> = s.splitn(2, ':').collect();
1551 if parts.len() == 2 {
1552 Some((
1553 Arc::from(parts[0]),
1554 parts[1].parse::<usize>().unwrap_or(0),
1555 ))
1556 } else {
1557 None
1558 }
1559 } else {
1560 None
1561 }
1562 })
1563 .collect();
1564 self.stack[base + a as usize] = VmValue::EnumDef(Arc::new(VmEnumDef {
1565 name: name.clone(),
1566 variants,
1567 }));
1568 } else {
1569 let field_names: Vec<Arc<str>> = fields_data
1570 .iter()
1571 .filter_map(|e| {
1572 if let AstExpr::String(s) = e {
1573 Some(Arc::from(s.as_str()))
1574 } else {
1575 None
1576 }
1577 })
1578 .collect();
1579 self.stack[base + a as usize] = VmValue::StructDef(Arc::new(VmStructDef {
1580 name: name.clone(),
1581 fields: field_names,
1582 }));
1583 }
1584 } else {
1585 let field_count = c as usize;
1587 let next_ip = self.frames[frame_idx].ip;
1589 let next = self.frames[frame_idx]
1590 .prototype
1591 .code
1592 .get(next_ip)
1593 .copied()
1594 .unwrap_or(0);
1595 let start_reg = decode_a(next) as usize;
1596 self.frames[frame_idx].ip += 1; let mut fields = Vec::new();
1599 for i in 0..field_count {
1600 let fname = self.stack[base + start_reg + i * 2].clone();
1601 let fval = self.stack[base + start_reg + i * 2 + 1].clone();
1602 let fname_str = match fname {
1603 VmValue::String(s) => s,
1604 _ => Arc::from(format!("field_{i}").as_str()),
1605 };
1606 fields.push((fname_str, fval));
1607 }
1608 self.stack[base + a as usize] =
1609 VmValue::StructInstance(Arc::new(VmStructInstance {
1610 type_name: name.clone(),
1611 fields,
1612 }));
1613 }
1614 }
1615
1616 Op::SetMember => {
1617 if matches!(&self.stack[base + a as usize], VmValue::Ref(_)) {
1618 return Err(runtime_err(
1619 "Cannot mutate a borrowed reference".to_string(),
1620 ));
1621 }
1622 let field_name = self.get_string_constant(frame_idx, b as u16)?;
1624 let val = self.stack[base + c as usize].clone();
1625 let obj = self.stack[base + a as usize].clone();
1626 if let VmValue::StructInstance(inst) = obj {
1627 let mut new_fields = inst.fields.clone();
1628 let mut found = false;
1629 for (k, v) in &mut new_fields {
1630 if k.as_ref() == field_name.as_ref() {
1631 *v = val.clone();
1632 found = true;
1633 break;
1634 }
1635 }
1636 if !found {
1637 new_fields.push((field_name, val));
1638 }
1639 self.stack[base + a as usize] =
1640 VmValue::StructInstance(Arc::new(VmStructInstance {
1641 type_name: inst.type_name.clone(),
1642 fields: new_fields,
1643 }));
1644 }
1645 }
1646
1647 Op::NewEnum => {
1648 let full_name = self.get_string_constant(frame_idx, b as u16)?;
1651 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1652 self.frames[frame_idx].ip += 1;
1653 let arg_count = decode_a(next) as usize;
1654 let args_start = c as usize;
1655
1656 let parts: Vec<&str> = full_name.splitn(2, "::").collect();
1658 let (type_name, variant) = if parts.len() == 2 {
1659 (Arc::from(parts[0]), Arc::from(parts[1]))
1660 } else {
1661 (Arc::from(""), Arc::from(full_name.as_ref()))
1662 };
1663
1664 let mut fields = Vec::new();
1665 for i in 0..arg_count {
1666 fields.push(self.stack[base + args_start + i].clone());
1667 }
1668
1669 self.stack[base + a as usize] = VmValue::EnumInstance(Arc::new(VmEnumInstance {
1670 type_name,
1671 variant,
1672 fields,
1673 }));
1674 }
1675
1676 Op::MatchEnum => {
1677 let variant_name = self.get_string_constant(frame_idx, b as u16)?;
1679 let subject = &self.stack[base + a as usize];
1680 let matched = match subject {
1681 VmValue::EnumInstance(e) => e.variant.as_ref() == variant_name.as_ref(),
1682 _ => false,
1683 };
1684 self.stack[base + c as usize] = VmValue::Bool(matched);
1685 }
1686
1687 Op::MethodCall => {
1688 let method_name = self.get_string_constant(frame_idx, c as u16)?;
1691 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1692 self.frames[frame_idx].ip += 1;
1693 let args_start = decode_a(next) as usize;
1694 let arg_count = decode_b(next) as usize;
1695
1696 let obj = self.stack[base + b as usize].clone();
1697 let mut args = Vec::new();
1698 for i in 0..arg_count {
1699 args.push(self.stack[base + args_start + i].clone());
1700 }
1701
1702 let result = self.dispatch_method(obj, &method_name, &args)?;
1703 self.stack[base + a as usize] = result;
1704 }
1705
1706 Op::Throw => {
1707 let val = self.stack[base + a as usize].clone();
1709 self.thrown_value = Some(val.clone());
1710 let err_msg = format!("{val}");
1711 return Err(runtime_err(err_msg));
1712 }
1713
1714 Op::TryBegin => {
1715 let catch_ip = (self.frames[frame_idx].ip as i32 + sbx as i32) as usize;
1717 self.try_handlers.push(TryHandler {
1718 frame_idx: self.frames.len(),
1719 catch_ip,
1720 });
1721 }
1722
1723 Op::TryEnd => {
1724 self.try_handlers.pop();
1726 }
1727
1728 Op::Import => {
1729 #[cfg(feature = "native")]
1730 {
1731 let path = self.get_string_constant(frame_idx, bx)?;
1736 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1737 self.frames[frame_idx].ip += 1;
1738 let next_a = decode_a(next);
1739 let next_b = decode_b(next);
1740 let next_c = decode_c(next);
1741
1742 let result = if next_c == 0xAB {
1743 self.handle_use_import(&path, next_a, next_b, frame_idx)?
1745 } else {
1746 let alias_idx = next_a as u16;
1748 let alias = self.get_string_constant(frame_idx, alias_idx)?;
1749 self.handle_import(&path, &alias)?
1750 };
1751 self.stack[base + a as usize] = result;
1752 }
1753 #[cfg(not(feature = "native"))]
1754 {
1755 let _ = (a, bx, frame_idx);
1756 return Err(runtime_err("Module imports not available in WASM"));
1757 }
1758 }
1759
1760 Op::Await => {
1761 let val = self.stack[base + b as usize].clone();
1763 match val {
1764 VmValue::Task(task) => {
1765 let rx = {
1766 let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
1767 guard.take()
1768 };
1769 match rx {
1770 Some(receiver) => match receiver.recv() {
1771 Ok(Ok(result)) => {
1772 self.stack[base + a as usize] = result;
1773 }
1774 Ok(Err(err_msg)) => {
1775 return Err(runtime_err(err_msg));
1776 }
1777 Err(_) => {
1778 return Err(runtime_err("Task channel disconnected"));
1779 }
1780 },
1781 None => {
1782 return Err(runtime_err("Task already awaited"));
1783 }
1784 }
1785 }
1786 other => {
1788 self.stack[base + a as usize] = other;
1789 }
1790 }
1791 }
1792 Op::Yield => {
1793 let val = self.stack[base + a as usize].clone();
1795 self.yielded_value = Some(val.clone());
1796 self.yielded_ip = self.frames[frame_idx].ip;
1798 self.frames.pop();
1800 return Ok(Some(val));
1801 }
1802 Op::TryPropagate => {
1803 let src = self.stack[base + b as usize].clone();
1809 match &src {
1810 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
1811 if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
1812 self.stack[base + a as usize] = ei.fields[0].clone();
1814 } else if ei.variant.as_ref() == "Err" {
1815 self.frames.pop();
1817 return Ok(Some(src));
1818 } else {
1819 self.stack[base + a as usize] = src;
1820 }
1821 }
1822 VmValue::None => {
1823 self.frames.pop();
1825 return Ok(Some(VmValue::None));
1826 }
1827 _ => {
1828 self.stack[base + a as usize] = src;
1830 }
1831 }
1832 }
1833 Op::ExtractField => {
1834 let source = self.stack[base + b as usize].clone();
1837 let is_rest = (c & 0x80) != 0;
1838 let idx = (c & 0x7F) as usize;
1839 let val = if is_rest {
1840 match &source {
1842 VmValue::List(l) => {
1843 if idx < l.len() {
1844 VmValue::List(Box::new(l[idx..].to_vec()))
1845 } else {
1846 VmValue::List(Box::default())
1847 }
1848 }
1849 _ => VmValue::List(Box::default()),
1850 }
1851 } else {
1852 match &source {
1853 VmValue::EnumInstance(ei) => {
1854 ei.fields.get(idx).cloned().unwrap_or(VmValue::None)
1855 }
1856 VmValue::List(l) => l.get(idx).cloned().unwrap_or(VmValue::None),
1857 _ => VmValue::None,
1858 }
1859 };
1860 self.stack[base + a as usize] = val;
1861 }
1862 Op::ExtractNamedField => {
1863 let source = self.stack[base + b as usize].clone();
1865 let field_name = match &self.frames[frame_idx].prototype.constants[c as usize] {
1866 Constant::String(s) => s.clone(),
1867 _ => return Err(runtime_err("ExtractNamedField: expected string constant")),
1868 };
1869 let val = match &source {
1870 VmValue::StructInstance(s) => s
1871 .fields
1872 .iter()
1873 .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1874 .map(|(_, v)| v.clone())
1875 .unwrap_or(VmValue::None),
1876 VmValue::Map(m) => m
1877 .iter()
1878 .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1879 .map(|(_, v)| v.clone())
1880 .unwrap_or(VmValue::None),
1881 _ => VmValue::None,
1882 };
1883 self.stack[base + a as usize] = val;
1884 }
1885
1886 Op::LoadMoved => {
1888 self.stack[base + a as usize] = VmValue::Moved;
1889 }
1890 Op::MakeRef => {
1891 let val = self.stack[base + b as usize].clone();
1892 self.stack[base + a as usize] = VmValue::Ref(Arc::new(val));
1893 }
1894 Op::ParallelFor => {
1895 }
1898 Op::AgentExec => {
1899 #[cfg(feature = "native")]
1900 {
1901 let result = self.handle_agent_exec(frame_idx, b, c)?;
1902 self.stack[base + a as usize] = result;
1903 }
1904 #[cfg(not(feature = "native"))]
1905 {
1906 let _ = (a, b, c, frame_idx);
1907 return Err(runtime_err("Agents not available in WASM".to_string()));
1908 }
1909 }
1910 }
1911 Ok(None)
1912 }
1913
1914 fn do_call(
1916 &mut self,
1917 func: VmValue,
1918 caller_base: usize,
1919 func_reg: u8,
1920 args_start: u8,
1921 arg_count: u8,
1922 ) -> Result<(), TlError> {
1923 const MAX_CALL_DEPTH: usize = 512;
1924 if self.frames.len() >= MAX_CALL_DEPTH {
1925 return Err(runtime_err(
1926 "Stack overflow: maximum recursion depth (512) exceeded",
1927 ));
1928 }
1929 match func {
1930 VmValue::Function(closure) => {
1931 let proto = closure.prototype.clone();
1932 let arity = proto.arity as usize;
1933
1934 if arg_count as usize != arity {
1935 return Err(runtime_err(format!(
1936 "Expected {} arguments, got {}",
1937 arity, arg_count
1938 )));
1939 }
1940
1941 if proto.is_generator {
1943 let mut closed_upvalues = Vec::new();
1945 for uv in &closure.upvalues {
1946 match uv {
1947 UpvalueRef::Open { stack_index } => {
1948 let val = self.stack[*stack_index].clone();
1949 closed_upvalues.push(UpvalueRef::Closed(val));
1950 }
1951 UpvalueRef::Closed(v) => {
1952 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
1953 }
1954 }
1955 }
1956
1957 let num_regs = proto.num_registers as usize;
1959 let mut saved_stack = vec![VmValue::None; num_regs];
1960 for (i, slot) in saved_stack.iter_mut().enumerate().take(arg_count as usize) {
1961 *slot = self.stack[caller_base + args_start as usize + i].clone();
1962 }
1963
1964 let gn = VmGenerator::new(GeneratorKind::UserDefined {
1965 prototype: proto,
1966 upvalues: closed_upvalues,
1967 saved_stack,
1968 ip: 0,
1969 });
1970 self.stack[caller_base + func_reg as usize] =
1971 VmValue::Generator(Arc::new(Mutex::new(gn)));
1972 return Ok(());
1973 }
1974
1975 let new_base = self.stack.len();
1977 self.ensure_stack(new_base + proto.num_registers as usize + 1);
1978
1979 for i in 0..arg_count as usize {
1981 self.stack[new_base + i] =
1982 self.stack[caller_base + args_start as usize + i].clone();
1983 }
1984
1985 self.frames.push(CallFrame {
1986 prototype: proto,
1987 ip: 0,
1988 base: new_base,
1989 upvalues: closure.upvalues.clone(),
1990 });
1991
1992 let result = self.run()?;
1994
1995 let result = self.close_upvalues_in_value(result, new_base);
1997
1998 self.stack[caller_base + func_reg as usize] = result;
2000
2001 self.stack.truncate(new_base);
2003
2004 Ok(())
2005 }
2006 VmValue::Builtin(builtin_id) => {
2007 let result = self.call_builtin(
2008 builtin_id as u16,
2009 caller_base + args_start as usize,
2010 arg_count as usize,
2011 )?;
2012 self.stack[caller_base + func_reg as usize] = result;
2013 Ok(())
2014 }
2015 _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
2016 }
2017 }
2018
2019 fn value_may_need_closing(val: &VmValue) -> bool {
2024 match val {
2025 VmValue::Function(_) => true,
2026 VmValue::List(items) => items.iter().any(Self::value_may_need_closing),
2027 VmValue::Map(entries) => entries.iter().any(|(_, v)| Self::value_may_need_closing(v)),
2028 _ => false,
2029 }
2030 }
2031
2032 fn close_upvalues_in_value(&self, val: VmValue, frame_base: usize) -> VmValue {
2033 match val {
2034 VmValue::Function(ref closure) => {
2035 let needs_closing = closure.upvalues.iter().any(|uv| {
2037 matches!(uv, UpvalueRef::Open { stack_index } if *stack_index >= frame_base)
2038 });
2039 if !needs_closing {
2040 return val;
2041 }
2042 let closed_upvalues: Vec<UpvalueRef> = closure
2043 .upvalues
2044 .iter()
2045 .map(|uv| match uv {
2046 UpvalueRef::Open { stack_index } if *stack_index >= frame_base => {
2047 UpvalueRef::Closed(self.stack[*stack_index].clone())
2048 }
2049 other => other.clone(),
2050 })
2051 .collect();
2052 VmValue::Function(Arc::new(VmClosure {
2053 prototype: closure.prototype.clone(),
2054 upvalues: closed_upvalues,
2055 }))
2056 }
2057 VmValue::List(items) => {
2058 if !items.iter().any(Self::value_may_need_closing) {
2059 return VmValue::List(items);
2060 }
2061 VmValue::List(Box::new(
2062 (*items)
2063 .into_iter()
2064 .map(|v| self.close_upvalues_in_value(v, frame_base))
2065 .collect(),
2066 ))
2067 }
2068 VmValue::Map(entries) => {
2069 if !entries.iter().any(|(_, v)| Self::value_may_need_closing(v)) {
2070 return VmValue::Map(entries);
2071 }
2072 VmValue::Map(Box::new(
2073 (*entries)
2074 .into_iter()
2075 .map(|(k, v)| (k, self.close_upvalues_in_value(v, frame_base)))
2076 .collect(),
2077 ))
2078 }
2079 other => other,
2080 }
2081 }
2082
2083 pub(crate) fn execute_closure(
2085 &mut self,
2086 proto: &Arc<Prototype>,
2087 upvalues: &[UpvalueRef],
2088 ) -> Result<VmValue, TlError> {
2089 let base = self.stack.len();
2090 self.ensure_stack(base + proto.num_registers as usize + 1);
2091 self.frames.push(CallFrame {
2092 prototype: proto.clone(),
2093 ip: 0,
2094 base,
2095 upvalues: upvalues.to_vec(),
2096 });
2097 self.run()
2098 }
2099
2100 pub(crate) fn execute_closure_with_args(
2102 &mut self,
2103 proto: &Arc<Prototype>,
2104 upvalues: &[UpvalueRef],
2105 args: &[VmValue],
2106 ) -> Result<VmValue, TlError> {
2107 let base = self.stack.len();
2108 self.ensure_stack(base + proto.num_registers as usize + 1);
2109 for (i, arg) in args.iter().enumerate() {
2110 self.stack[base + i] = arg.clone();
2111 }
2112 self.frames.push(CallFrame {
2113 prototype: proto.clone(),
2114 ip: 0,
2115 base,
2116 upvalues: upvalues.to_vec(),
2117 });
2118 self.run()
2119 }
2120
2121 fn load_constant(&self, frame_idx: usize, idx: u16) -> Result<VmValue, TlError> {
2122 let frame = &self.frames[frame_idx];
2123 match &frame.prototype.constants[idx as usize] {
2124 Constant::Int(n) => Ok(VmValue::Int(*n)),
2125 Constant::Float(f) => Ok(VmValue::Float(*f)),
2126 Constant::String(s) => Ok(VmValue::String(s.clone())),
2127 Constant::Prototype(p) => {
2128 Ok(VmValue::Function(Arc::new(VmClosure {
2130 prototype: p.clone(),
2131 upvalues: Vec::new(),
2132 })))
2133 }
2134 Constant::Decimal(s) => {
2135 use std::str::FromStr;
2136 Ok(VmValue::Decimal(
2137 rust_decimal::Decimal::from_str(s).unwrap_or_default(),
2138 ))
2139 }
2140 Constant::AstExpr(_) | Constant::AstExprList(_) => Ok(VmValue::None),
2141 }
2142 }
2143
2144 fn get_string_constant(&self, frame_idx: usize, idx: u16) -> Result<Arc<str>, TlError> {
2145 let frame = &self.frames[frame_idx];
2146 match &frame.prototype.constants[idx as usize] {
2147 Constant::String(s) => Ok(s.clone()),
2148 _ => Err(runtime_err("Expected string constant")),
2149 }
2150 }
2151
2152 fn vm_add(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2155 let left = &self.stack[base + b as usize];
2156 let right = &self.stack[base + c as usize];
2157 match (left, right) {
2158 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2159 .checked_add(*b)
2160 .map(VmValue::Int)
2161 .unwrap_or_else(|| VmValue::Float(*a as f64 + *b as f64))),
2162 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a + b)),
2163 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 + b)),
2164 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a + *b as f64)),
2165 (VmValue::String(a), VmValue::String(b)) => {
2166 Ok(VmValue::String(Arc::from(format!("{a}{b}").as_str())))
2167 }
2168 #[cfg(feature = "gpu")]
2169 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2170 let a = a.clone();
2171 let b = b.clone();
2172 let ops = self.get_gpu_ops()?;
2173 let result = ops.add(&a, &b).map_err(runtime_err)?;
2174 Ok(VmValue::GpuTensor(Arc::new(result)))
2175 }
2176 #[cfg(feature = "gpu")]
2177 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2178 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2179 let lv = self.stack[base + b as usize].clone();
2180 let rv = self.stack[base + c as usize].clone();
2181 let a = self.ensure_gpu_tensor(&lv)?;
2182 let b_val = self.ensure_gpu_tensor(&rv)?;
2183 let ops = self.get_gpu_ops()?;
2184 let result = ops.add(&a, &b_val).map_err(runtime_err)?;
2185 Ok(VmValue::GpuTensor(Arc::new(result)))
2186 }
2187 #[cfg(feature = "native")]
2188 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2189 let result = a.add(b).map_err(|e| runtime_err(e.to_string()))?;
2190 Ok(VmValue::Tensor(Arc::new(result)))
2191 }
2192 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a + b)),
2194 (VmValue::Decimal(a), VmValue::Int(b)) => {
2195 Ok(VmValue::Decimal(a + rust_decimal::Decimal::from(*b)))
2196 }
2197 (VmValue::Int(a), VmValue::Decimal(b)) => {
2198 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) + b))
2199 }
2200 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) + b)),
2201 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a + decimal_to_f64(b))),
2202 _ => Err(runtime_err(format!(
2203 "Cannot apply `+` to {} and {}",
2204 left.type_name(),
2205 right.type_name()
2206 ))),
2207 }
2208 }
2209
2210 fn vm_sub(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2211 let left = &self.stack[base + b as usize];
2212 let right = &self.stack[base + c as usize];
2213 match (left, right) {
2214 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2215 .checked_sub(*b)
2216 .map(VmValue::Int)
2217 .unwrap_or_else(|| VmValue::Float(*a as f64 - *b as f64))),
2218 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a - b)),
2219 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 - b)),
2220 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a - *b as f64)),
2221 #[cfg(feature = "gpu")]
2222 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2223 let a = a.clone();
2224 let b = b.clone();
2225 let ops = self.get_gpu_ops()?;
2226 let result = ops.sub(&a, &b).map_err(runtime_err)?;
2227 Ok(VmValue::GpuTensor(Arc::new(result)))
2228 }
2229 #[cfg(feature = "gpu")]
2230 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2231 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2232 let lv = self.stack[base + b as usize].clone();
2233 let rv = self.stack[base + c as usize].clone();
2234 let a = self.ensure_gpu_tensor(&lv)?;
2235 let b_val = self.ensure_gpu_tensor(&rv)?;
2236 let ops = self.get_gpu_ops()?;
2237 let result = ops.sub(&a, &b_val).map_err(runtime_err)?;
2238 Ok(VmValue::GpuTensor(Arc::new(result)))
2239 }
2240 #[cfg(feature = "native")]
2241 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2242 let result = a.sub(b).map_err(|e| runtime_err(e.to_string()))?;
2243 Ok(VmValue::Tensor(Arc::new(result)))
2244 }
2245 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a - b)),
2246 (VmValue::Decimal(a), VmValue::Int(b)) => {
2247 Ok(VmValue::Decimal(a - rust_decimal::Decimal::from(*b)))
2248 }
2249 (VmValue::Int(a), VmValue::Decimal(b)) => {
2250 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) - b))
2251 }
2252 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) - b)),
2253 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a - decimal_to_f64(b))),
2254 _ => Err(runtime_err(format!(
2255 "Cannot apply `-` to {} and {}",
2256 left.type_name(),
2257 right.type_name()
2258 ))),
2259 }
2260 }
2261
2262 fn vm_mul(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2263 let left = &self.stack[base + b as usize];
2264 let right = &self.stack[base + c as usize];
2265 match (left, right) {
2266 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2267 .checked_mul(*b)
2268 .map(VmValue::Int)
2269 .unwrap_or_else(|| VmValue::Float(*a as f64 * *b as f64))),
2270 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a * b)),
2271 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 * b)),
2272 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a * *b as f64)),
2273 (VmValue::String(a), VmValue::Int(b)) => {
2274 if *b < 0 {
2275 return Err(runtime_err(
2276 "Cannot repeat string a negative number of times",
2277 ));
2278 }
2279 if *b > 10_000_000 {
2280 return Err(runtime_err(
2281 "String repeat count too large (max 10,000,000)",
2282 ));
2283 }
2284 Ok(VmValue::String(Arc::from(a.repeat(*b as usize).as_str())))
2285 }
2286 #[cfg(feature = "gpu")]
2287 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2288 let a = a.clone();
2289 let b = b.clone();
2290 let ops = self.get_gpu_ops()?;
2291 let result = ops.mul(&a, &b).map_err(runtime_err)?;
2292 Ok(VmValue::GpuTensor(Arc::new(result)))
2293 }
2294 #[cfg(feature = "gpu")]
2295 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2296 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2297 let lv = self.stack[base + b as usize].clone();
2298 let rv = self.stack[base + c as usize].clone();
2299 let a = self.ensure_gpu_tensor(&lv)?;
2300 let b_val = self.ensure_gpu_tensor(&rv)?;
2301 let ops = self.get_gpu_ops()?;
2302 let result = ops.mul(&a, &b_val).map_err(runtime_err)?;
2303 Ok(VmValue::GpuTensor(Arc::new(result)))
2304 }
2305 #[cfg(feature = "gpu")]
2306 (VmValue::GpuTensor(t), VmValue::Float(s))
2307 | (VmValue::Float(s), VmValue::GpuTensor(t)) => {
2308 let t = t.clone();
2309 let s = *s;
2310 let ops = self.get_gpu_ops()?;
2311 let result = ops.scale(&t, s as f32);
2312 Ok(VmValue::GpuTensor(Arc::new(result)))
2313 }
2314 #[cfg(feature = "native")]
2315 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2316 let result = a.mul(b).map_err(|e| runtime_err(e.to_string()))?;
2317 Ok(VmValue::Tensor(Arc::new(result)))
2318 }
2319 #[cfg(feature = "native")]
2320 (VmValue::Tensor(t), VmValue::Float(s)) | (VmValue::Float(s), VmValue::Tensor(t)) => {
2321 let result = t.scale(*s);
2322 Ok(VmValue::Tensor(Arc::new(result)))
2323 }
2324 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a * b)),
2325 (VmValue::Decimal(a), VmValue::Int(b)) => {
2326 Ok(VmValue::Decimal(a * rust_decimal::Decimal::from(*b)))
2327 }
2328 (VmValue::Int(a), VmValue::Decimal(b)) => {
2329 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) * b))
2330 }
2331 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) * b)),
2332 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a * decimal_to_f64(b))),
2333 _ => Err(runtime_err(format!(
2334 "Cannot apply `*` to {} and {}",
2335 left.type_name(),
2336 right.type_name()
2337 ))),
2338 }
2339 }
2340
2341 fn vm_div(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2342 let left = &self.stack[base + b as usize];
2343 let right = &self.stack[base + c as usize];
2344 match (left, right) {
2345 (VmValue::Int(a), VmValue::Int(b)) => {
2346 if *b == 0 {
2347 return Err(runtime_err("Division by zero"));
2348 }
2349 Ok(VmValue::Int(a / b))
2350 }
2351 (VmValue::Float(a), VmValue::Float(b)) => {
2352 if *b == 0.0 {
2353 return Err(runtime_err("Division by zero"));
2354 }
2355 Ok(VmValue::Float(a / b))
2356 }
2357 (VmValue::Int(a), VmValue::Float(b)) => {
2358 if *b == 0.0 {
2359 return Err(runtime_err("Division by zero"));
2360 }
2361 Ok(VmValue::Float(*a as f64 / b))
2362 }
2363 (VmValue::Float(a), VmValue::Int(b)) => {
2364 if *b == 0 {
2365 return Err(runtime_err("Division by zero"));
2366 }
2367 Ok(VmValue::Float(a / *b as f64))
2368 }
2369 #[cfg(feature = "gpu")]
2370 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2371 let a = a.clone();
2372 let b = b.clone();
2373 let ops = self.get_gpu_ops()?;
2374 let result = ops.div(&a, &b).map_err(runtime_err)?;
2375 Ok(VmValue::GpuTensor(Arc::new(result)))
2376 }
2377 #[cfg(feature = "gpu")]
2378 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2379 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2380 let lv = self.stack[base + b as usize].clone();
2381 let rv = self.stack[base + c as usize].clone();
2382 let a = self.ensure_gpu_tensor(&lv)?;
2383 let b_val = self.ensure_gpu_tensor(&rv)?;
2384 let ops = self.get_gpu_ops()?;
2385 let result = ops.div(&a, &b_val).map_err(runtime_err)?;
2386 Ok(VmValue::GpuTensor(Arc::new(result)))
2387 }
2388 #[cfg(feature = "native")]
2389 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2390 let result = a.div(b).map_err(|e| runtime_err(e.to_string()))?;
2391 Ok(VmValue::Tensor(Arc::new(result)))
2392 }
2393 (VmValue::Decimal(a), VmValue::Decimal(b)) => {
2394 if b.is_zero() {
2395 return Err(runtime_err("Division by zero"));
2396 }
2397 Ok(VmValue::Decimal(a / b))
2398 }
2399 (VmValue::Decimal(a), VmValue::Int(b)) => {
2400 if *b == 0 {
2401 return Err(runtime_err("Division by zero"));
2402 }
2403 Ok(VmValue::Decimal(a / rust_decimal::Decimal::from(*b)))
2404 }
2405 (VmValue::Int(a), VmValue::Decimal(b)) => {
2406 if b.is_zero() {
2407 return Err(runtime_err("Division by zero"));
2408 }
2409 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) / b))
2410 }
2411 (VmValue::Decimal(a), VmValue::Float(b)) => {
2412 if *b == 0.0 {
2413 return Err(runtime_err("Division by zero"));
2414 }
2415 Ok(VmValue::Float(decimal_to_f64(a) / b))
2416 }
2417 (VmValue::Float(a), VmValue::Decimal(b)) => {
2418 if b.is_zero() {
2419 return Err(runtime_err("Division by zero"));
2420 }
2421 Ok(VmValue::Float(a / decimal_to_f64(b)))
2422 }
2423 _ => Err(runtime_err(format!(
2424 "Cannot apply `/` to {} and {}",
2425 left.type_name(),
2426 right.type_name()
2427 ))),
2428 }
2429 }
2430
2431 fn vm_mod(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2432 let left = &self.stack[base + b as usize];
2433 let right = &self.stack[base + c as usize];
2434 match (left, right) {
2435 (VmValue::Int(a), VmValue::Int(b)) => {
2436 if *b == 0 {
2437 return Err(runtime_err("Modulo by zero"));
2438 }
2439 Ok(VmValue::Int(a % b))
2440 }
2441 (VmValue::Float(a), VmValue::Float(b)) => {
2442 if *b == 0.0 {
2443 return Err(runtime_err("Modulo by zero"));
2444 }
2445 Ok(VmValue::Float(a % b))
2446 }
2447 (VmValue::Int(a), VmValue::Float(b)) => {
2448 if *b == 0.0 {
2449 return Err(runtime_err("Modulo by zero"));
2450 }
2451 Ok(VmValue::Float(*a as f64 % b))
2452 }
2453 (VmValue::Float(a), VmValue::Int(b)) => {
2454 if *b == 0 {
2455 return Err(runtime_err("Modulo by zero"));
2456 }
2457 Ok(VmValue::Float(a % *b as f64))
2458 }
2459 _ => Err(runtime_err(format!(
2460 "Cannot apply `%` to {} and {}",
2461 left.type_name(),
2462 right.type_name()
2463 ))),
2464 }
2465 }
2466
2467 fn vm_pow(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2468 let left = &self.stack[base + b as usize];
2469 let right = &self.stack[base + c as usize];
2470 match (left, right) {
2471 (VmValue::Int(a), VmValue::Int(b)) => {
2472 if *b < 0 {
2473 return Ok(VmValue::Float((*a as f64).powi(*b as i32)));
2474 }
2475 match a.checked_pow(*b as u32) {
2476 Some(result) => Ok(VmValue::Int(result)),
2477 None => Ok(VmValue::Float((*a as f64).powf(*b as f64))),
2478 }
2479 }
2480 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
2481 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float((*a as f64).powf(*b))),
2482 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a.powf(*b as f64))),
2483 _ => Err(runtime_err(format!(
2484 "Cannot apply `**` to {} and {}",
2485 left.type_name(),
2486 right.type_name()
2487 ))),
2488 }
2489 }
2490
2491 fn vm_eq(&self, base: usize, b: u8, c: u8) -> bool {
2492 self.stack[base + b as usize] == self.stack[base + c as usize]
2493 }
2494
2495 fn vm_cmp(&self, base: usize, b: u8, c: u8) -> Result<Option<i8>, TlError> {
2496 let left = &self.stack[base + b as usize];
2497 let right = &self.stack[base + c as usize];
2498 match (left, right) {
2499 (VmValue::Int(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2500 (VmValue::Float(a), VmValue::Float(b)) => Ok(a.partial_cmp(b).map(|o| o as i8)),
2501 (VmValue::Int(a), VmValue::Float(b)) => {
2502 let fa = *a as f64;
2503 Ok(fa.partial_cmp(b).map(|o| o as i8))
2504 }
2505 (VmValue::Float(a), VmValue::Int(b)) => {
2506 let fb = *b as f64;
2507 Ok(a.partial_cmp(&fb).map(|o| o as i8))
2508 }
2509 (VmValue::String(a), VmValue::String(b)) => Ok(Some(a.cmp(b) as i8)),
2510 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(Some(a.cmp(b) as i8)),
2511 (VmValue::Decimal(a), VmValue::Int(b)) => {
2512 Ok(Some(a.cmp(&rust_decimal::Decimal::from(*b)) as i8))
2513 }
2514 (VmValue::Int(a), VmValue::Decimal(b)) => {
2515 Ok(Some(rust_decimal::Decimal::from(*a).cmp(b) as i8))
2516 }
2517 (VmValue::DateTime(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2518 (VmValue::DateTime(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2519 (VmValue::Int(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2520 _ => Err(runtime_err(format!(
2521 "Cannot compare {} and {}",
2522 left.type_name(),
2523 right.type_name()
2524 ))),
2525 }
2526 }
2527
2528 fn check_permission(&self, perm: &str) -> Result<(), TlError> {
2531 if let Some(ref policy) = self.security_policy
2532 && !policy.check(perm)
2533 {
2534 return Err(runtime_err(format!("{perm} blocked by security policy")));
2535 }
2536 Ok(())
2537 }
2538
2539 pub fn call_builtin(
2542 &mut self,
2543 id: u16,
2544 args_base: usize,
2545 arg_count: usize,
2546 ) -> Result<VmValue, TlError> {
2547 let args: Vec<VmValue> = (0..arg_count)
2548 .map(|i| {
2549 let val = &self.stack[args_base + i];
2550 match val {
2552 VmValue::Ref(inner) => inner.as_ref().clone(),
2553 other => other.clone(),
2554 }
2555 })
2556 .collect();
2557
2558 let builtin_id: BuiltinId =
2559 BuiltinId::try_from(id).map_err(|v| runtime_err(format!("Invalid builtin id: {v}")))?;
2560
2561 match builtin_id {
2562 BuiltinId::Print | BuiltinId::Println => {
2563 let mut parts = Vec::new();
2564 for a in &args {
2565 #[cfg(feature = "native")]
2566 match a {
2567 VmValue::Table(t) => {
2568 let batches =
2569 self.engine().collect(t.df.clone()).map_err(runtime_err)?;
2570 let formatted =
2571 DataEngine::format_batches(&batches).map_err(runtime_err)?;
2572 parts.push(formatted);
2573 }
2574 _ => parts.push(format!("{a}")),
2575 }
2576 #[cfg(not(feature = "native"))]
2577 parts.push(format!("{a}"));
2578 }
2579 let line = parts.join(" ");
2580 println!("{line}");
2581 self.output.push(line);
2582 Ok(VmValue::None)
2583 }
2584 BuiltinId::Len => match args.first() {
2585 Some(VmValue::String(s)) => Ok(VmValue::Int(s.len() as i64)),
2586 Some(VmValue::List(l)) => Ok(VmValue::Int(l.len() as i64)),
2587 Some(VmValue::Map(pairs)) => Ok(VmValue::Int(pairs.len() as i64)),
2588 Some(VmValue::Set(items)) => Ok(VmValue::Int(items.len() as i64)),
2589 _ => Err(runtime_err("len() expects a string, list, map, or set")),
2590 },
2591 BuiltinId::Str => Ok(VmValue::String(Arc::from(
2592 args.first()
2593 .map(|v| format!("{v}"))
2594 .unwrap_or_default()
2595 .as_str(),
2596 ))),
2597 BuiltinId::Int => match args.first() {
2598 Some(VmValue::Float(f)) => Ok(VmValue::Int(*f as i64)),
2599 Some(VmValue::String(s)) => s
2600 .parse::<i64>()
2601 .map(VmValue::Int)
2602 .map_err(|_| runtime_err(format!("Cannot convert '{s}' to int"))),
2603 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
2604 Some(VmValue::Bool(b)) => Ok(VmValue::Int(if *b { 1 } else { 0 })),
2605 _ => Err(runtime_err("int() expects a number, string, or bool")),
2606 },
2607 BuiltinId::Float => match args.first() {
2608 Some(VmValue::Int(n)) => Ok(VmValue::Float(*n as f64)),
2609 Some(VmValue::String(s)) => s
2610 .parse::<f64>()
2611 .map(VmValue::Float)
2612 .map_err(|_| runtime_err(format!("Cannot convert '{s}' to float"))),
2613 Some(VmValue::Float(n)) => Ok(VmValue::Float(*n)),
2614 Some(VmValue::Bool(b)) => Ok(VmValue::Float(if *b { 1.0 } else { 0.0 })),
2615 _ => Err(runtime_err("float() expects a number, string, or bool")),
2616 },
2617 BuiltinId::Abs => match args.first() {
2618 Some(VmValue::Int(n)) => Ok(VmValue::Int(n.abs())),
2619 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.abs())),
2620 _ => Err(runtime_err("abs() expects a number")),
2621 },
2622 BuiltinId::Min => {
2623 if args.len() == 2 {
2624 match (&args[0], &args[1]) {
2625 (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.min(b))),
2626 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.min(*b))),
2627 _ => Err(runtime_err("min() expects two numbers")),
2628 }
2629 } else {
2630 Err(runtime_err("min() expects 2 arguments"))
2631 }
2632 }
2633 BuiltinId::Max => {
2634 if args.len() == 2 {
2635 match (&args[0], &args[1]) {
2636 (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.max(b))),
2637 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.max(*b))),
2638 _ => Err(runtime_err("max() expects two numbers")),
2639 }
2640 } else {
2641 Err(runtime_err("max() expects 2 arguments"))
2642 }
2643 }
2644 BuiltinId::Range => {
2645 if args.len() == 1 {
2646 if let VmValue::Int(n) = &args[0] {
2647 if *n > 10_000_000 {
2648 return Err(runtime_err("range() size too large (max 10,000,000)"));
2649 }
2650 if *n < 0 {
2651 return Ok(VmValue::List(Box::default()));
2652 }
2653 Ok(VmValue::List(Box::new((0..*n).map(VmValue::Int).collect())))
2654 } else {
2655 Err(runtime_err("range() expects an integer"))
2656 }
2657 } else if args.len() == 2 {
2658 if let (VmValue::Int(start), VmValue::Int(end)) = (&args[0], &args[1]) {
2659 let size = (*end - *start).max(0);
2660 if size > 10_000_000 {
2661 return Err(runtime_err("range() size too large (max 10,000,000)"));
2662 }
2663 Ok(VmValue::List(Box::new(
2664 (*start..*end).map(VmValue::Int).collect(),
2665 )))
2666 } else {
2667 Err(runtime_err("range() expects integers"))
2668 }
2669 } else if args.len() == 3 {
2670 if let (VmValue::Int(start), VmValue::Int(end), VmValue::Int(step)) =
2671 (&args[0], &args[1], &args[2])
2672 {
2673 if *step == 0 {
2674 return Err(runtime_err("range() step cannot be zero"));
2675 }
2676 let mut result = Vec::new();
2677 let mut i = *start;
2678 if *step > 0 {
2679 while i < *end {
2680 result.push(VmValue::Int(i));
2681 i += step;
2682 }
2683 } else {
2684 while i > *end {
2685 result.push(VmValue::Int(i));
2686 i += step;
2687 }
2688 }
2689 Ok(VmValue::List(Box::new(result)))
2690 } else {
2691 Err(runtime_err("range() expects integers"))
2692 }
2693 } else {
2694 Err(runtime_err("range() expects 1, 2, or 3 arguments"))
2695 }
2696 }
2697 BuiltinId::Push => {
2698 if args.len() == 2 {
2699 if let VmValue::List(mut items) = args[0].clone() {
2700 items.push(args[1].clone());
2701 Ok(VmValue::List(items))
2702 } else {
2703 Err(runtime_err("push() first arg must be a list"))
2704 }
2705 } else {
2706 Err(runtime_err("push() expects 2 arguments"))
2707 }
2708 }
2709 BuiltinId::TypeOf => Ok(VmValue::String(Arc::from(
2710 args.first().map(|v| v.type_name()).unwrap_or("none"),
2711 ))),
2712 BuiltinId::Map => {
2713 if args.len() != 2 {
2714 return Err(runtime_err("map() expects 2 arguments (list, fn)"));
2715 }
2716 let items = match &args[0] {
2717 VmValue::List(items) => (**items).clone(),
2718 _ => return Err(runtime_err("map() first arg must be a list")),
2719 };
2720 let func = args[1].clone();
2721 #[cfg(feature = "native")]
2723 if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2724 let proto = match &func {
2725 VmValue::Function(c) => c.prototype.clone(),
2726 _ => unreachable!(),
2727 };
2728 let result: Result<Vec<VmValue>, TlError> = items
2729 .into_par_iter()
2730 .map(|item| execute_pure_fn(&proto, &[item]))
2731 .collect();
2732 return Ok(VmValue::List(Box::new(result?)));
2733 }
2734 let mut result = Vec::new();
2735 for item in items {
2736 let val = self.call_vm_function(&func, &[item])?;
2737 result.push(val);
2738 }
2739 Ok(VmValue::List(Box::new(result)))
2740 }
2741 BuiltinId::Filter => {
2742 if args.len() != 2 {
2743 return Err(runtime_err("filter() expects 2 arguments (list, fn)"));
2744 }
2745 let items = match &args[0] {
2746 VmValue::List(items) => (**items).clone(),
2747 _ => return Err(runtime_err("filter() first arg must be a list")),
2748 };
2749 let func = args[1].clone();
2750 #[cfg(feature = "native")]
2752 if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2753 let proto = match &func {
2754 VmValue::Function(c) => c.prototype.clone(),
2755 _ => unreachable!(),
2756 };
2757 let result: Result<Vec<VmValue>, TlError> = items
2758 .into_par_iter()
2759 .filter_map(|item| {
2760 match execute_pure_fn(&proto, std::slice::from_ref(&item)) {
2761 Ok(val) => {
2762 if val.is_truthy() {
2763 Some(Ok(item))
2764 } else {
2765 None
2766 }
2767 }
2768 Err(e) => Some(Err(e)),
2769 }
2770 })
2771 .collect();
2772 return Ok(VmValue::List(Box::new(result?)));
2773 }
2774 let mut result = Vec::new();
2775 for item in items {
2776 let val = self.call_vm_function(&func, std::slice::from_ref(&item))?;
2777 if val.is_truthy() {
2778 result.push(item);
2779 }
2780 }
2781 Ok(VmValue::List(Box::new(result)))
2782 }
2783 BuiltinId::Reduce | BuiltinId::Fold => {
2784 if args.len() != 3 {
2785 return Err(runtime_err(
2786 "reduce()/fold() expects 3 arguments (list, init, fn)",
2787 ));
2788 }
2789 let items = match &args[0] {
2790 VmValue::List(items) => (**items).clone(),
2791 _ => return Err(runtime_err("reduce() first arg must be a list")),
2792 };
2793 let mut acc = args[1].clone();
2794 let func = args[2].clone();
2795 for item in items {
2796 acc = self.call_vm_function(&func, &[acc, item])?;
2797 }
2798 Ok(acc)
2799 }
2800 BuiltinId::Sum => {
2801 if args.len() != 1 {
2802 return Err(runtime_err("sum() expects 1 argument (list)"));
2803 }
2804 let items = match &args[0] {
2805 VmValue::List(items) => items,
2806 _ => return Err(runtime_err("sum() expects a list")),
2807 };
2808 let has_float = items.iter().any(|v| matches!(v, VmValue::Float(_)));
2810 #[cfg(feature = "native")]
2811 if items.len() >= PARALLEL_THRESHOLD {
2812 if has_float {
2814 let total: f64 = items
2815 .par_iter()
2816 .map(|v| match v {
2817 VmValue::Int(n) => *n as f64,
2818 VmValue::Float(n) => *n,
2819 _ => 0.0,
2820 })
2821 .sum();
2822 return Ok(VmValue::Float(total));
2823 } else {
2824 let total: i64 = items
2825 .par_iter()
2826 .map(|v| match v {
2827 VmValue::Int(n) => *n,
2828 _ => 0,
2829 })
2830 .sum();
2831 return Ok(VmValue::Int(total));
2832 }
2833 }
2834 let mut total: i64 = 0;
2836 let mut is_float = false;
2837 let mut total_f: f64 = 0.0;
2838 for item in items.iter() {
2839 match item {
2840 VmValue::Int(n) => {
2841 if is_float {
2842 total_f += *n as f64;
2843 } else {
2844 total += n;
2845 }
2846 }
2847 VmValue::Float(n) => {
2848 if !is_float {
2849 total_f = total as f64;
2850 is_float = true;
2851 }
2852 total_f += n;
2853 }
2854 _ => return Err(runtime_err("sum() list must contain numbers")),
2855 }
2856 }
2857 if is_float {
2858 Ok(VmValue::Float(total_f))
2859 } else {
2860 Ok(VmValue::Int(total))
2861 }
2862 }
2863 BuiltinId::Any => {
2864 if args.len() != 2 {
2865 return Err(runtime_err("any() expects 2 arguments (list, fn)"));
2866 }
2867 let items = match &args[0] {
2868 VmValue::List(items) => (**items).clone(),
2869 _ => return Err(runtime_err("any() first arg must be a list")),
2870 };
2871 let func = args[1].clone();
2872 for item in items {
2873 let val = self.call_vm_function(&func, &[item])?;
2874 if val.is_truthy() {
2875 return Ok(VmValue::Bool(true));
2876 }
2877 }
2878 Ok(VmValue::Bool(false))
2879 }
2880 BuiltinId::All => {
2881 if args.len() != 2 {
2882 return Err(runtime_err("all() expects 2 arguments (list, fn)"));
2883 }
2884 let items = match &args[0] {
2885 VmValue::List(items) => (**items).clone(),
2886 _ => return Err(runtime_err("all() first arg must be a list")),
2887 };
2888 let func = args[1].clone();
2889 for item in items {
2890 let val = self.call_vm_function(&func, &[item])?;
2891 if !val.is_truthy() {
2892 return Ok(VmValue::Bool(false));
2893 }
2894 }
2895 Ok(VmValue::Bool(true))
2896 }
2897 #[cfg(feature = "native")]
2899 BuiltinId::ReadCsv => {
2900 if args.len() != 1 {
2901 return Err(runtime_err("read_csv() expects 1 argument (path)"));
2902 }
2903 let path = match &args[0] {
2904 VmValue::String(s) => s.to_string(),
2905 _ => return Err(runtime_err("read_csv() path must be a string")),
2906 };
2907 match self.engine().read_csv(&path) {
2908 Ok(df) => Ok(VmValue::Table(VmTable { df })),
2909 Err(e) => {
2910 let msg = e.to_string();
2911 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2912 type_name: Arc::from("DataError"),
2913 variant: Arc::from("ParseError"),
2914 fields: vec![
2915 VmValue::String(Arc::from(msg.as_str())),
2916 VmValue::String(Arc::from(path.as_str())),
2917 ],
2918 })));
2919 Err(runtime_err(msg))
2920 }
2921 }
2922 }
2923 #[cfg(feature = "native")]
2924 BuiltinId::ReadParquet => {
2925 if args.len() != 1 {
2926 return Err(runtime_err("read_parquet() expects 1 argument (path)"));
2927 }
2928 let path = match &args[0] {
2929 VmValue::String(s) => s.to_string(),
2930 _ => return Err(runtime_err("read_parquet() path must be a string")),
2931 };
2932 match self.engine().read_parquet(&path) {
2933 Ok(df) => Ok(VmValue::Table(VmTable { df })),
2934 Err(e) => {
2935 let msg = e.to_string();
2936 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2937 type_name: Arc::from("DataError"),
2938 variant: Arc::from("ParseError"),
2939 fields: vec![
2940 VmValue::String(Arc::from(msg.as_str())),
2941 VmValue::String(Arc::from(path.as_str())),
2942 ],
2943 })));
2944 Err(runtime_err(msg))
2945 }
2946 }
2947 }
2948 #[cfg(feature = "native")]
2949 BuiltinId::WriteCsv => {
2950 if args.len() != 2 {
2951 return Err(runtime_err("write_csv() expects 2 arguments (table, path)"));
2952 }
2953 let df = match &args[0] {
2954 VmValue::Table(t) => t.df.clone(),
2955 _ => return Err(runtime_err("write_csv() first arg must be a table")),
2956 };
2957 let path = match &args[1] {
2958 VmValue::String(s) => s.to_string(),
2959 _ => return Err(runtime_err("write_csv() path must be a string")),
2960 };
2961 match self.engine().write_csv(df, &path) {
2962 Ok(_) => Ok(VmValue::None),
2963 Err(e) => {
2964 let msg = e.to_string();
2965 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2966 type_name: Arc::from("DataError"),
2967 variant: Arc::from("ParseError"),
2968 fields: vec![
2969 VmValue::String(Arc::from(msg.as_str())),
2970 VmValue::String(Arc::from(path.as_str())),
2971 ],
2972 })));
2973 Err(runtime_err(msg))
2974 }
2975 }
2976 }
2977 #[cfg(feature = "native")]
2978 BuiltinId::WriteParquet => {
2979 if args.len() != 2 {
2980 return Err(runtime_err(
2981 "write_parquet() expects 2 arguments (table, path)",
2982 ));
2983 }
2984 let df = match &args[0] {
2985 VmValue::Table(t) => t.df.clone(),
2986 _ => return Err(runtime_err("write_parquet() first arg must be a table")),
2987 };
2988 let path = match &args[1] {
2989 VmValue::String(s) => s.to_string(),
2990 _ => return Err(runtime_err("write_parquet() path must be a string")),
2991 };
2992 match self.engine().write_parquet(df, &path) {
2993 Ok(_) => Ok(VmValue::None),
2994 Err(e) => {
2995 let msg = e.to_string();
2996 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2997 type_name: Arc::from("DataError"),
2998 variant: Arc::from("ParseError"),
2999 fields: vec![
3000 VmValue::String(Arc::from(msg.as_str())),
3001 VmValue::String(Arc::from(path.as_str())),
3002 ],
3003 })));
3004 Err(runtime_err(msg))
3005 }
3006 }
3007 }
3008 #[cfg(feature = "native")]
3009 BuiltinId::Collect => {
3010 if args.len() != 1 {
3011 return Err(runtime_err("collect() expects 1 argument (table)"));
3012 }
3013 let df = match &args[0] {
3014 VmValue::Table(t) => t.df.clone(),
3015 _ => return Err(runtime_err("collect() expects a table")),
3016 };
3017 let batches = self.engine().collect(df).map_err(runtime_err)?;
3018 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3019 Ok(VmValue::String(Arc::from(formatted.as_str())))
3020 }
3021 #[cfg(feature = "native")]
3022 BuiltinId::Show => {
3023 let df = match args.first() {
3024 Some(VmValue::Table(t)) => t.df.clone(),
3025 _ => return Err(runtime_err("show() expects a table")),
3026 };
3027 let limit = match args.get(1) {
3028 Some(VmValue::Int(n)) => *n as usize,
3029 None => 20,
3030 _ => return Err(runtime_err("show() second arg must be an int")),
3031 };
3032 let limited = df
3033 .limit(0, Some(limit))
3034 .map_err(|e| runtime_err(format!("{e}")))?;
3035 let batches = self.engine().collect(limited).map_err(runtime_err)?;
3036 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3037 println!("{formatted}");
3038 self.output.push(formatted);
3039 Ok(VmValue::None)
3040 }
3041 #[cfg(feature = "native")]
3042 BuiltinId::Describe => {
3043 if args.len() != 1 {
3044 return Err(runtime_err("describe() expects 1 argument (table)"));
3045 }
3046 let df = match &args[0] {
3047 VmValue::Table(t) => t.df.clone(),
3048 _ => return Err(runtime_err("describe() expects a table")),
3049 };
3050 let schema = df.schema();
3051 let mut lines = Vec::new();
3052 lines.push("Columns:".to_string());
3053 for (qualifier, field) in schema.iter() {
3054 let prefix = match qualifier {
3055 Some(q) => format!("{q}."),
3056 None => String::new(),
3057 };
3058 lines.push(format!(
3059 " {}{}: {}",
3060 prefix,
3061 field.name(),
3062 field.data_type()
3063 ));
3064 }
3065 let output = lines.join("\n");
3066 println!("{output}");
3067 self.output.push(output.clone());
3068 Ok(VmValue::String(Arc::from(output.as_str())))
3069 }
3070 #[cfg(feature = "native")]
3071 BuiltinId::Head => {
3072 if args.is_empty() {
3073 return Err(runtime_err("head() expects at least 1 argument (table)"));
3074 }
3075 let df = match &args[0] {
3076 VmValue::Table(t) => t.df.clone(),
3077 _ => return Err(runtime_err("head() first arg must be a table")),
3078 };
3079 let n = match args.get(1) {
3080 Some(VmValue::Int(n)) => *n as usize,
3081 None => 10,
3082 _ => return Err(runtime_err("head() second arg must be an int")),
3083 };
3084 let limited = df
3085 .limit(0, Some(n))
3086 .map_err(|e| runtime_err(format!("{e}")))?;
3087 Ok(VmValue::Table(VmTable { df: limited }))
3088 }
3089 #[cfg(feature = "native")]
3090 BuiltinId::Postgres => {
3091 if args.len() != 2 {
3092 return Err(runtime_err(
3093 "postgres() expects 2 arguments (conn_str, table_name)",
3094 ));
3095 }
3096 let conn_str = match &args[0] {
3097 VmValue::String(s) => s.to_string(),
3098 _ => return Err(runtime_err("postgres() conn_str must be a string")),
3099 };
3100 let table_name = match &args[1] {
3101 VmValue::String(s) => s.to_string(),
3102 _ => return Err(runtime_err("postgres() table_name must be a string")),
3103 };
3104 let conn_str = resolve_tl_config_connection(&conn_str);
3105 match self.engine().read_postgres(&conn_str, &table_name) {
3106 Ok(df) => Ok(VmValue::Table(VmTable { df })),
3107 Err(e) => {
3108 let msg = e.to_string();
3109 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3110 type_name: Arc::from("ConnectorError"),
3111 variant: Arc::from("QueryError"),
3112 fields: vec![
3113 VmValue::String(Arc::from(msg.as_str())),
3114 VmValue::String(Arc::from("postgres")),
3115 ],
3116 })));
3117 Err(runtime_err(msg))
3118 }
3119 }
3120 }
3121 #[cfg(feature = "native")]
3122 BuiltinId::PostgresQuery => {
3123 if args.len() != 2 {
3124 return Err(runtime_err(
3125 "postgres_query() expects 2 arguments (conn_str, query)",
3126 ));
3127 }
3128 let conn_str = match &args[0] {
3129 VmValue::String(s) => s.to_string(),
3130 _ => return Err(runtime_err("postgres_query() conn_str must be a string")),
3131 };
3132 let query = match &args[1] {
3133 VmValue::String(s) => s.to_string(),
3134 _ => return Err(runtime_err("postgres_query() query must be a string")),
3135 };
3136 let conn_str = resolve_tl_config_connection(&conn_str);
3137 match self
3138 .engine()
3139 .query_postgres(&conn_str, &query, "__pg_query_result")
3140 {
3141 Ok(df) => Ok(VmValue::Table(VmTable { df })),
3142 Err(e) => {
3143 let msg = e.to_string();
3144 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3145 type_name: Arc::from("ConnectorError"),
3146 variant: Arc::from("QueryError"),
3147 fields: vec![
3148 VmValue::String(Arc::from(msg.as_str())),
3149 VmValue::String(Arc::from("postgres")),
3150 ],
3151 })));
3152 Err(runtime_err(msg))
3153 }
3154 }
3155 }
3156 BuiltinId::TlConfigResolve => {
3157 if args.len() != 1 {
3158 return Err(runtime_err("tl_config_resolve() expects 1 argument (name)"));
3159 }
3160 let name = match &args[0] {
3161 VmValue::String(s) => s.to_string(),
3162 _ => return Err(runtime_err("tl_config_resolve() name must be a string")),
3163 };
3164 let resolved = resolve_tl_config_connection(&name);
3165 Ok(VmValue::String(Arc::from(resolved.as_str())))
3166 }
3167 #[cfg(not(feature = "native"))]
3168 BuiltinId::ReadCsv
3169 | BuiltinId::ReadParquet
3170 | BuiltinId::WriteCsv
3171 | BuiltinId::WriteParquet
3172 | BuiltinId::Collect
3173 | BuiltinId::Show
3174 | BuiltinId::Describe
3175 | BuiltinId::Head
3176 | BuiltinId::Postgres
3177 | BuiltinId::PostgresQuery => Err(runtime_err("Data operations not available in WASM")),
3178 #[cfg(feature = "native")]
3180 BuiltinId::Tensor => {
3181 if args.is_empty() {
3182 return Err(runtime_err("tensor() expects at least 1 argument"));
3183 }
3184 let data = self.vmvalue_to_f64_list(&args[0])?;
3185 let shape = if args.len() > 1 {
3186 self.vmvalue_to_usize_list(&args[1])?
3187 } else {
3188 vec![data.len()]
3189 };
3190 let t = tl_ai::TlTensor::from_vec(data, &shape)
3191 .map_err(|e| runtime_err(e.to_string()))?;
3192 Ok(VmValue::Tensor(Arc::new(t)))
3193 }
3194 #[cfg(feature = "native")]
3195 BuiltinId::TensorZeros => {
3196 if args.is_empty() {
3197 return Err(runtime_err("tensor_zeros() expects 1 argument (shape)"));
3198 }
3199 let shape = self.vmvalue_to_usize_list(&args[0])?;
3200 let t = tl_ai::TlTensor::zeros(&shape);
3201 Ok(VmValue::Tensor(Arc::new(t)))
3202 }
3203 #[cfg(feature = "native")]
3204 BuiltinId::TensorOnes => {
3205 if args.is_empty() {
3206 return Err(runtime_err("tensor_ones() expects 1 argument (shape)"));
3207 }
3208 let shape = self.vmvalue_to_usize_list(&args[0])?;
3209 let t = tl_ai::TlTensor::ones(&shape);
3210 Ok(VmValue::Tensor(Arc::new(t)))
3211 }
3212 #[cfg(feature = "native")]
3213 BuiltinId::TensorShape => match args.first() {
3214 Some(VmValue::Tensor(t)) => {
3215 let shape: Vec<VmValue> =
3216 t.shape().iter().map(|&d| VmValue::Int(d as i64)).collect();
3217 Ok(VmValue::List(Box::new(shape)))
3218 }
3219 _ => Err(runtime_err("tensor_shape() expects a tensor")),
3220 },
3221 #[cfg(feature = "native")]
3222 BuiltinId::TensorReshape => {
3223 if args.len() != 2 {
3224 return Err(runtime_err(
3225 "tensor_reshape() expects 2 arguments (tensor, shape)",
3226 ));
3227 }
3228 let t = match &args[0] {
3229 VmValue::Tensor(t) => (**t).clone(),
3230 _ => return Err(runtime_err("tensor_reshape() first arg must be a tensor")),
3231 };
3232 let shape = self.vmvalue_to_usize_list(&args[1])?;
3233 let reshaped = t.reshape(&shape).map_err(|e| runtime_err(e.to_string()))?;
3234 Ok(VmValue::Tensor(Arc::new(reshaped)))
3235 }
3236 #[cfg(feature = "native")]
3237 BuiltinId::TensorTranspose => match args.first() {
3238 Some(VmValue::Tensor(t)) => {
3239 let transposed = t.transpose().map_err(|e| runtime_err(e.to_string()))?;
3240 Ok(VmValue::Tensor(Arc::new(transposed)))
3241 }
3242 _ => Err(runtime_err("tensor_transpose() expects a tensor")),
3243 },
3244 #[cfg(feature = "native")]
3245 BuiltinId::TensorSum => match args.first() {
3246 Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.sum())),
3247 _ => Err(runtime_err("tensor_sum() expects a tensor")),
3248 },
3249 #[cfg(feature = "native")]
3250 BuiltinId::TensorMean => match args.first() {
3251 Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.mean())),
3252 _ => Err(runtime_err("tensor_mean() expects a tensor")),
3253 },
3254 #[cfg(feature = "native")]
3255 BuiltinId::TensorDot => {
3256 if args.len() != 2 {
3257 return Err(runtime_err("tensor_dot() expects 2 arguments"));
3258 }
3259 let a_t = match &args[0] {
3260 VmValue::Tensor(t) => t,
3261 _ => return Err(runtime_err("tensor_dot() first arg must be a tensor")),
3262 };
3263 let b_t = match &args[1] {
3264 VmValue::Tensor(t) => t,
3265 _ => return Err(runtime_err("tensor_dot() second arg must be a tensor")),
3266 };
3267 let result = a_t.dot(b_t).map_err(|e| runtime_err(e.to_string()))?;
3268 Ok(VmValue::Tensor(Arc::new(result)))
3269 }
3270 #[cfg(feature = "native")]
3271 BuiltinId::Predict => {
3272 if args.len() < 2 {
3273 return Err(runtime_err(
3274 "predict() expects at least 2 arguments (model, input)",
3275 ));
3276 }
3277 let model = match &args[0] {
3278 VmValue::Model(m) => (**m).clone(),
3279 _ => return Err(runtime_err("predict() first arg must be a model")),
3280 };
3281 let input = match &args[1] {
3282 VmValue::Tensor(t) => (**t).clone(),
3283 _ => return Err(runtime_err("predict() second arg must be a tensor")),
3284 };
3285 let result =
3286 tl_ai::predict(&model, &input).map_err(|e| runtime_err(e.to_string()))?;
3287 Ok(VmValue::Tensor(Arc::new(result)))
3288 }
3289 #[cfg(feature = "native")]
3290 BuiltinId::Similarity => {
3291 if args.len() != 2 {
3292 return Err(runtime_err("similarity() expects 2 arguments"));
3293 }
3294 let a_t = match &args[0] {
3295 VmValue::Tensor(t) => t,
3296 _ => return Err(runtime_err("similarity() first arg must be a tensor")),
3297 };
3298 let b_t = match &args[1] {
3299 VmValue::Tensor(t) => t,
3300 _ => return Err(runtime_err("similarity() second arg must be a tensor")),
3301 };
3302 let sim = tl_ai::similarity(a_t, b_t).map_err(|e| runtime_err(e.to_string()))?;
3303 Ok(VmValue::Float(sim))
3304 }
3305 #[cfg(feature = "native")]
3306 BuiltinId::AiComplete => {
3307 if args.is_empty() {
3308 return Err(runtime_err(
3309 "ai_complete() expects at least 1 argument (prompt)",
3310 ));
3311 }
3312 let prompt = match &args[0] {
3313 VmValue::String(s) => s.to_string(),
3314 _ => return Err(runtime_err("ai_complete() first arg must be a string")),
3315 };
3316 let model = match args.get(1) {
3317 Some(VmValue::String(s)) => Some(s.to_string()),
3318 _ => None,
3319 };
3320 let result = tl_ai::ai_complete(&prompt, model.as_deref(), None, None)
3321 .map_err(|e| runtime_err(e.to_string()))?;
3322 Ok(VmValue::String(Arc::from(result.as_str())))
3323 }
3324 #[cfg(feature = "native")]
3325 BuiltinId::AiChat => {
3326 if args.is_empty() {
3327 return Err(runtime_err("ai_chat() expects at least 1 argument (model)"));
3328 }
3329 let model = match &args[0] {
3330 VmValue::String(s) => s.to_string(),
3331 _ => return Err(runtime_err("ai_chat() first arg must be a string (model)")),
3332 };
3333 let system = match args.get(1) {
3334 Some(VmValue::String(s)) => Some(s.to_string()),
3335 _ => None,
3336 };
3337 let messages: Vec<(String, String)> = if let Some(VmValue::List(msgs)) = args.get(2)
3338 {
3339 msgs.chunks(2)
3340 .filter_map(|chunk| {
3341 if chunk.len() == 2
3342 && let (VmValue::String(role), VmValue::String(content)) =
3343 (&chunk[0], &chunk[1])
3344 {
3345 return Some((role.to_string(), content.to_string()));
3346 }
3347 None
3348 })
3349 .collect()
3350 } else {
3351 Vec::new()
3352 };
3353 let result = tl_ai::ai_chat(&model, system.as_deref(), &messages)
3354 .map_err(|e| runtime_err(e.to_string()))?;
3355 Ok(VmValue::String(Arc::from(result.as_str())))
3356 }
3357 #[cfg(feature = "native")]
3358 BuiltinId::ModelSave => {
3359 if args.len() != 2 {
3360 return Err(runtime_err(
3361 "model_save() expects 2 arguments (model, path)",
3362 ));
3363 }
3364 let model = match &args[0] {
3365 VmValue::Model(m) => m,
3366 _ => return Err(runtime_err("model_save() first arg must be a model")),
3367 };
3368 let path = match &args[1] {
3369 VmValue::String(s) => s.to_string(),
3370 _ => return Err(runtime_err("model_save() second arg must be a string path")),
3371 };
3372 model
3373 .save(std::path::Path::new(&path))
3374 .map_err(|e| runtime_err(e.to_string()))?;
3375 Ok(VmValue::None)
3376 }
3377 #[cfg(feature = "native")]
3378 BuiltinId::ModelLoad => {
3379 if args.is_empty() {
3380 return Err(runtime_err("model_load() expects 1 argument (path)"));
3381 }
3382 let path = match &args[0] {
3383 VmValue::String(s) => s.to_string(),
3384 _ => return Err(runtime_err("model_load() arg must be a string path")),
3385 };
3386 let model = tl_ai::TlModel::load(std::path::Path::new(&path))
3387 .map_err(|e| runtime_err(e.to_string()))?;
3388 Ok(VmValue::Model(Arc::new(model)))
3389 }
3390 #[cfg(feature = "native")]
3391 BuiltinId::ModelRegister => {
3392 if args.len() != 2 {
3393 return Err(runtime_err(
3394 "model_register() expects 2 arguments (name, model)",
3395 ));
3396 }
3397 let name = match &args[0] {
3398 VmValue::String(s) => s.to_string(),
3399 _ => return Err(runtime_err("model_register() first arg must be a string")),
3400 };
3401 let model = match &args[1] {
3402 VmValue::Model(m) => (**m).clone(),
3403 _ => return Err(runtime_err("model_register() second arg must be a model")),
3404 };
3405 let registry = tl_ai::ModelRegistry::default_location();
3406 registry
3407 .register(&name, &model)
3408 .map_err(|e| runtime_err(e.to_string()))?;
3409 Ok(VmValue::None)
3410 }
3411 #[cfg(feature = "native")]
3412 BuiltinId::ModelList => {
3413 let registry = tl_ai::ModelRegistry::default_location();
3414 let names = registry.list();
3415 let items: Vec<VmValue> = names
3416 .into_iter()
3417 .map(|n: String| VmValue::String(Arc::from(n.as_str())))
3418 .collect();
3419 Ok(VmValue::List(Box::new(items)))
3420 }
3421 #[cfg(feature = "native")]
3422 BuiltinId::ModelGet => {
3423 if args.is_empty() {
3424 return Err(runtime_err("model_get() expects 1 argument (name)"));
3425 }
3426 let name = match &args[0] {
3427 VmValue::String(s) => s.to_string(),
3428 _ => return Err(runtime_err("model_get() arg must be a string")),
3429 };
3430 let registry = tl_ai::ModelRegistry::default_location();
3431 match registry.get(&name) {
3432 Ok(m) => Ok(VmValue::Model(Arc::new(m))),
3433 Err(_) => Ok(VmValue::None),
3434 }
3435 }
3436 #[cfg(not(feature = "native"))]
3437 BuiltinId::Tensor
3438 | BuiltinId::TensorZeros
3439 | BuiltinId::TensorOnes
3440 | BuiltinId::TensorShape
3441 | BuiltinId::TensorReshape
3442 | BuiltinId::TensorTranspose
3443 | BuiltinId::TensorSum
3444 | BuiltinId::TensorMean
3445 | BuiltinId::TensorDot
3446 | BuiltinId::Predict
3447 | BuiltinId::Similarity
3448 | BuiltinId::AiComplete
3449 | BuiltinId::AiChat
3450 | BuiltinId::ModelSave
3451 | BuiltinId::ModelLoad
3452 | BuiltinId::ModelRegister
3453 | BuiltinId::ModelList
3454 | BuiltinId::ModelGet => Err(runtime_err("AI/ML operations not available in WASM")),
3455 #[cfg(feature = "native")]
3457 BuiltinId::AlertSlack => {
3458 if args.len() < 2 {
3459 return Err(runtime_err("alert_slack(url, msg) requires 2 args"));
3460 }
3461 let url = match &args[0] {
3462 VmValue::String(s) => s.to_string(),
3463 _ => return Err(runtime_err("alert_slack: url must be a string")),
3464 };
3465 let msg = format!("{}", args[1]);
3466 tl_stream::send_alert(&tl_stream::AlertTarget::Slack(url), &msg)
3467 .map_err(|e| runtime_err(&e))?;
3468 Ok(VmValue::None)
3469 }
3470 #[cfg(feature = "native")]
3471 BuiltinId::AlertWebhook => {
3472 if args.len() < 2 {
3473 return Err(runtime_err("alert_webhook(url, msg) requires 2 args"));
3474 }
3475 let url = match &args[0] {
3476 VmValue::String(s) => s.to_string(),
3477 _ => return Err(runtime_err("alert_webhook: url must be a string")),
3478 };
3479 let msg = format!("{}", args[1]);
3480 tl_stream::send_alert(&tl_stream::AlertTarget::Webhook(url), &msg)
3481 .map_err(|e| runtime_err(&e))?;
3482 Ok(VmValue::None)
3483 }
3484 #[cfg(feature = "native")]
3485 BuiltinId::Emit => {
3486 if args.is_empty() {
3487 return Err(runtime_err("emit() requires at least 1 argument"));
3488 }
3489 self.output.push(format!("emit: {}", args[0]));
3490 Ok(args[0].clone())
3491 }
3492 #[cfg(feature = "native")]
3493 BuiltinId::Lineage => Ok(VmValue::String(Arc::from("lineage_tracker"))),
3494 #[cfg(feature = "native")]
3495 BuiltinId::RunPipeline => {
3496 if args.is_empty() {
3497 return Err(runtime_err("run_pipeline() requires a pipeline"));
3498 }
3499 if let VmValue::PipelineDef(ref def) = args[0] {
3500 Ok(VmValue::String(Arc::from(
3501 format!("Pipeline '{}' triggered", def.name).as_str(),
3502 )))
3503 } else {
3504 Err(runtime_err("run_pipeline: argument must be a pipeline"))
3505 }
3506 }
3507 #[cfg(not(feature = "native"))]
3508 BuiltinId::AlertSlack
3509 | BuiltinId::AlertWebhook
3510 | BuiltinId::Emit
3511 | BuiltinId::Lineage
3512 | BuiltinId::RunPipeline => Err(runtime_err("Streaming not available in WASM")),
3513 BuiltinId::Sqrt => match args.first() {
3515 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sqrt())),
3516 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sqrt())),
3517 _ => Err(runtime_err("sqrt() expects a number")),
3518 },
3519 BuiltinId::Pow => {
3520 if args.len() == 2 {
3521 match (&args[0], &args[1]) {
3522 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
3523 (VmValue::Int(a), VmValue::Int(b)) => {
3524 Ok(VmValue::Float((*a as f64).powf(*b as f64)))
3525 }
3526 (VmValue::Float(a), VmValue::Int(b)) => {
3527 Ok(VmValue::Float(a.powf(*b as f64)))
3528 }
3529 (VmValue::Int(a), VmValue::Float(b)) => {
3530 Ok(VmValue::Float((*a as f64).powf(*b)))
3531 }
3532 _ => Err(runtime_err("pow() expects two numbers")),
3533 }
3534 } else {
3535 Err(runtime_err("pow() expects 2 arguments"))
3536 }
3537 }
3538 BuiltinId::Floor => match args.first() {
3539 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.floor())),
3540 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3541 _ => Err(runtime_err("floor() expects a number")),
3542 },
3543 BuiltinId::Ceil => match args.first() {
3544 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ceil())),
3545 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3546 _ => Err(runtime_err("ceil() expects a number")),
3547 },
3548 BuiltinId::Round => match args.first() {
3549 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.round())),
3550 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3551 _ => Err(runtime_err("round() expects a number")),
3552 },
3553 BuiltinId::Sin => match args.first() {
3554 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sin())),
3555 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sin())),
3556 _ => Err(runtime_err("sin() expects a number")),
3557 },
3558 BuiltinId::Cos => match args.first() {
3559 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.cos())),
3560 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).cos())),
3561 _ => Err(runtime_err("cos() expects a number")),
3562 },
3563 BuiltinId::Tan => match args.first() {
3564 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.tan())),
3565 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).tan())),
3566 _ => Err(runtime_err("tan() expects a number")),
3567 },
3568 BuiltinId::Log => match args.first() {
3569 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ln())),
3570 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).ln())),
3571 _ => Err(runtime_err("log() expects a number")),
3572 },
3573 BuiltinId::Log2 => match args.first() {
3574 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log2())),
3575 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log2())),
3576 _ => Err(runtime_err("log2() expects a number")),
3577 },
3578 BuiltinId::Log10 => match args.first() {
3579 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log10())),
3580 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log10())),
3581 _ => Err(runtime_err("log10() expects a number")),
3582 },
3583 BuiltinId::Join => {
3584 if args.len() == 2 {
3585 if let (VmValue::String(sep), VmValue::List(items)) = (&args[0], &args[1]) {
3586 let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
3587 Ok(VmValue::String(Arc::from(
3588 parts.join(sep.as_ref()).as_str(),
3589 )))
3590 } else {
3591 Err(runtime_err("join() expects separator and list"))
3592 }
3593 } else {
3594 Err(runtime_err("join() expects 2 arguments"))
3595 }
3596 }
3597 #[cfg(feature = "native")]
3598 BuiltinId::HttpGet => {
3599 self.check_permission("network")?;
3600 if args.is_empty() {
3601 return Err(runtime_err("http_get() expects a URL"));
3602 }
3603 if let VmValue::String(url) = &args[0] {
3604 match reqwest::blocking::get(url.as_ref()).and_then(|r| r.text()) {
3605 Ok(body) => Ok(VmValue::String(Arc::from(body.as_str()))),
3606 Err(e) => {
3607 let msg = format!("HTTP GET error: {e}");
3608 self.thrown_value =
3609 Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3610 type_name: Arc::from("NetworkError"),
3611 variant: Arc::from("HttpError"),
3612 fields: vec![
3613 VmValue::String(Arc::from(msg.as_str())),
3614 VmValue::String(url.clone()),
3615 ],
3616 })));
3617 Err(runtime_err(msg))
3618 }
3619 }
3620 } else {
3621 Err(runtime_err("http_get() expects a string URL"))
3622 }
3623 }
3624 #[cfg(feature = "native")]
3625 BuiltinId::HttpPost => {
3626 self.check_permission("network")?;
3627 if args.len() < 2 {
3628 return Err(runtime_err("http_post() expects URL and body"));
3629 }
3630 if let (VmValue::String(url), VmValue::String(body)) = (&args[0], &args[1]) {
3631 let client = reqwest::blocking::Client::new();
3632 match client
3633 .post(url.as_ref())
3634 .header("Content-Type", "application/json")
3635 .body(body.to_string())
3636 .send()
3637 .and_then(|r| r.text())
3638 {
3639 Ok(resp) => Ok(VmValue::String(Arc::from(resp.as_str()))),
3640 Err(e) => {
3641 let msg = format!("HTTP POST error: {e}");
3642 self.thrown_value =
3643 Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3644 type_name: Arc::from("NetworkError"),
3645 variant: Arc::from("HttpError"),
3646 fields: vec![
3647 VmValue::String(Arc::from(msg.as_str())),
3648 VmValue::String(url.clone()),
3649 ],
3650 })));
3651 Err(runtime_err(msg))
3652 }
3653 }
3654 } else {
3655 Err(runtime_err("http_post() expects string URL and body"))
3656 }
3657 }
3658 #[cfg(not(feature = "native"))]
3659 BuiltinId::HttpGet | BuiltinId::HttpPost => {
3660 Err(runtime_err("HTTP requests not available in WASM"))
3661 }
3662 BuiltinId::Assert => {
3663 if args.is_empty() {
3664 return Err(runtime_err("assert() expects at least 1 argument"));
3665 }
3666 if !args[0].is_truthy() {
3667 let msg = if args.len() > 1 {
3668 format!("{}", args[1])
3669 } else {
3670 "Assertion failed".to_string()
3671 };
3672 Err(runtime_err(msg))
3673 } else {
3674 Ok(VmValue::None)
3675 }
3676 }
3677 BuiltinId::AssertEq => {
3678 if args.len() < 2 {
3679 return Err(runtime_err("assert_eq() expects 2 arguments"));
3680 }
3681 let eq = match (&args[0], &args[1]) {
3682 (VmValue::Int(a), VmValue::Int(b)) => a == b,
3683 (VmValue::Float(a), VmValue::Float(b)) => a == b,
3684 (VmValue::String(a), VmValue::String(b)) => a == b,
3685 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
3686 (VmValue::None, VmValue::None) => true,
3687 _ => false,
3688 };
3689 if !eq {
3690 Err(runtime_err(format!(
3691 "Assertion failed: {} != {}",
3692 args[0], args[1]
3693 )))
3694 } else {
3695 Ok(VmValue::None)
3696 }
3697 }
3698 BuiltinId::JsonParse => {
3700 if args.is_empty() {
3701 return Err(runtime_err("json_parse() expects a string"));
3702 }
3703 if let VmValue::String(s) = &args[0] {
3704 let json_val: serde_json::Value = serde_json::from_str(s)
3705 .map_err(|e| runtime_err(format!("JSON parse error: {e}")))?;
3706 Ok(vm_json_to_value(&json_val))
3707 } else {
3708 Err(runtime_err("json_parse() expects a string"))
3709 }
3710 }
3711 BuiltinId::JsonStringify => {
3712 if args.is_empty() {
3713 return Err(runtime_err("json_stringify() expects a value"));
3714 }
3715 let json = vm_value_to_json(&args[0]);
3716 Ok(VmValue::String(Arc::from(json.to_string().as_str())))
3717 }
3718 BuiltinId::MapFrom => {
3719 if !args.len().is_multiple_of(2) {
3720 return Err(runtime_err(
3721 "map_from() expects even number of arguments (key, value pairs)",
3722 ));
3723 }
3724 let mut pairs = Vec::new();
3725 for chunk in args.chunks(2) {
3726 let key = match &chunk[0] {
3727 VmValue::String(s) => s.clone(),
3728 other => Arc::from(format!("{other}").as_str()),
3729 };
3730 pairs.push((key, chunk[1].clone()));
3731 }
3732 Ok(VmValue::Map(Box::new(pairs)))
3733 }
3734 #[cfg(feature = "native")]
3735 BuiltinId::ReadFile => {
3736 self.check_permission("file_read")?;
3737 if args.is_empty() {
3738 return Err(runtime_err("read_file() expects a path"));
3739 }
3740 if let VmValue::String(path) = &args[0] {
3741 let content = std::fs::read_to_string(path.as_ref())
3742 .map_err(|e| runtime_err(format!("read_file error: {e}")))?;
3743 Ok(VmValue::String(Arc::from(content.as_str())))
3744 } else {
3745 Err(runtime_err("read_file() expects a string path"))
3746 }
3747 }
3748 #[cfg(feature = "native")]
3749 BuiltinId::WriteFile => {
3750 self.check_permission("file_write")?;
3751 if args.len() < 2 {
3752 return Err(runtime_err("write_file() expects path and content"));
3753 }
3754 if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3755 std::fs::write(path.as_ref(), content.as_ref())
3756 .map_err(|e| runtime_err(format!("write_file error: {e}")))?;
3757 Ok(VmValue::None)
3758 } else {
3759 Err(runtime_err("write_file() expects string path and content"))
3760 }
3761 }
3762 #[cfg(feature = "native")]
3763 BuiltinId::AppendFile => {
3764 self.check_permission("file_write")?;
3765 if args.len() < 2 {
3766 return Err(runtime_err("append_file() expects path and content"));
3767 }
3768 if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3769 use std::io::Write;
3770 let mut file = std::fs::OpenOptions::new()
3771 .create(true)
3772 .append(true)
3773 .open(path.as_ref())
3774 .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3775 file.write_all(content.as_bytes())
3776 .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3777 Ok(VmValue::None)
3778 } else {
3779 Err(runtime_err("append_file() expects string path and content"))
3780 }
3781 }
3782 #[cfg(feature = "native")]
3783 BuiltinId::FileExists => {
3784 self.check_permission("file_read")?;
3785 if args.is_empty() {
3786 return Err(runtime_err("file_exists() expects a path"));
3787 }
3788 if let VmValue::String(path) = &args[0] {
3789 Ok(VmValue::Bool(std::path::Path::new(path.as_ref()).exists()))
3790 } else {
3791 Err(runtime_err("file_exists() expects a string path"))
3792 }
3793 }
3794 #[cfg(feature = "native")]
3795 BuiltinId::ListDir => {
3796 self.check_permission("file_read")?;
3797 if args.is_empty() {
3798 return Err(runtime_err("list_dir() expects a path"));
3799 }
3800 if let VmValue::String(path) = &args[0] {
3801 let entries: Vec<VmValue> = std::fs::read_dir(path.as_ref())
3802 .map_err(|e| runtime_err(format!("list_dir error: {e}")))?
3803 .filter_map(|e| e.ok())
3804 .map(|e| {
3805 VmValue::String(Arc::from(e.file_name().to_string_lossy().as_ref()))
3806 })
3807 .collect();
3808 Ok(VmValue::List(Box::new(entries)))
3809 } else {
3810 Err(runtime_err("list_dir() expects a string path"))
3811 }
3812 }
3813 #[cfg(not(feature = "native"))]
3814 BuiltinId::ReadFile
3815 | BuiltinId::WriteFile
3816 | BuiltinId::AppendFile
3817 | BuiltinId::FileExists
3818 | BuiltinId::ListDir => Err(runtime_err("File I/O not available in WASM")),
3819 #[cfg(feature = "native")]
3820 BuiltinId::EnvGet => {
3821 if args.is_empty() {
3822 return Err(runtime_err("env_get() expects a name"));
3823 }
3824 if let VmValue::String(name) = &args[0] {
3825 match std::env::var(name.as_ref()) {
3826 Ok(val) => Ok(VmValue::String(Arc::from(val.as_str()))),
3827 Err(_) => Ok(VmValue::None),
3828 }
3829 } else {
3830 Err(runtime_err("env_get() expects a string"))
3831 }
3832 }
3833 #[cfg(feature = "native")]
3834 BuiltinId::EnvSet => {
3835 self.check_permission("env_write")?;
3836 if args.len() < 2 {
3837 return Err(runtime_err("env_set() expects name and value"));
3838 }
3839 if let (VmValue::String(name), VmValue::String(val)) = (&args[0], &args[1]) {
3840 let _guard = env_lock();
3841 unsafe {
3842 std::env::set_var(name.as_ref(), val.as_ref());
3843 }
3844 Ok(VmValue::None)
3845 } else {
3846 Err(runtime_err("env_set() expects two strings"))
3847 }
3848 }
3849 #[cfg(not(feature = "native"))]
3850 BuiltinId::EnvGet | BuiltinId::EnvSet => {
3851 Err(runtime_err("Environment variables not available in WASM"))
3852 }
3853 BuiltinId::RegexMatch => {
3854 if args.len() < 2 {
3855 return Err(runtime_err("regex_match() expects pattern and string"));
3856 }
3857 if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3858 if pattern.len() > 10_000 {
3859 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3860 }
3861 let re = regex::RegexBuilder::new(pattern)
3862 .size_limit(10_000_000)
3863 .build()
3864 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3865 Ok(VmValue::Bool(re.is_match(text)))
3866 } else {
3867 Err(runtime_err(
3868 "regex_match() expects string pattern and string",
3869 ))
3870 }
3871 }
3872 BuiltinId::RegexFind => {
3873 if args.len() < 2 {
3874 return Err(runtime_err("regex_find() expects pattern and string"));
3875 }
3876 if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3877 if pattern.len() > 10_000 {
3878 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3879 }
3880 let re = regex::RegexBuilder::new(pattern)
3881 .size_limit(10_000_000)
3882 .build()
3883 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3884 let matches: Vec<VmValue> = re
3885 .find_iter(text)
3886 .map(|m| VmValue::String(Arc::from(m.as_str())))
3887 .collect();
3888 Ok(VmValue::List(Box::new(matches)))
3889 } else {
3890 Err(runtime_err(
3891 "regex_find() expects string pattern and string",
3892 ))
3893 }
3894 }
3895 BuiltinId::RegexReplace => {
3896 if args.len() < 3 {
3897 return Err(runtime_err(
3898 "regex_replace() expects pattern, string, replacement",
3899 ));
3900 }
3901 if let (
3902 VmValue::String(pattern),
3903 VmValue::String(text),
3904 VmValue::String(replacement),
3905 ) = (&args[0], &args[1], &args[2])
3906 {
3907 if pattern.len() > 10_000 {
3908 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3909 }
3910 let re = regex::RegexBuilder::new(pattern)
3911 .size_limit(10_000_000)
3912 .build()
3913 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3914 Ok(VmValue::String(Arc::from(
3915 re.replace_all(text, replacement.as_ref()).as_ref(),
3916 )))
3917 } else {
3918 Err(runtime_err("regex_replace() expects three strings"))
3919 }
3920 }
3921 BuiltinId::Now => {
3922 let ts = chrono::Utc::now().timestamp_millis();
3923 Ok(VmValue::DateTime(ts))
3924 }
3925 BuiltinId::DateFormat => {
3926 if args.len() < 2 {
3927 return Err(runtime_err(
3928 "date_format() expects datetime/timestamp and format",
3929 ));
3930 }
3931 let ts = match &args[0] {
3932 VmValue::DateTime(ms) => *ms,
3933 VmValue::Int(ms) => *ms,
3934 _ => {
3935 return Err(runtime_err(
3936 "date_format() expects a datetime or int timestamp",
3937 ));
3938 }
3939 };
3940 let fmt = match &args[1] {
3941 VmValue::String(s) => s,
3942 _ => return Err(runtime_err("date_format() expects a string format")),
3943 };
3944 use chrono::TimeZone;
3945 let secs = ts / 1000;
3946 let nsecs = ((ts % 1000) * 1_000_000) as u32;
3947 let dt = chrono::Utc
3948 .timestamp_opt(secs, nsecs)
3949 .single()
3950 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
3951 Ok(VmValue::String(Arc::from(
3952 dt.format(fmt.as_ref()).to_string().as_str(),
3953 )))
3954 }
3955 BuiltinId::DateParse => {
3956 if args.len() < 2 {
3957 return Err(runtime_err("date_parse() expects string and format"));
3958 }
3959 if let (VmValue::String(s), VmValue::String(fmt)) = (&args[0], &args[1]) {
3960 let dt = chrono::NaiveDateTime::parse_from_str(s, fmt)
3961 .map_err(|e| runtime_err(format!("date_parse error: {e}")))?;
3962 let ts = dt.and_utc().timestamp_millis();
3963 Ok(VmValue::DateTime(ts))
3964 } else {
3965 Err(runtime_err("date_parse() expects two strings"))
3966 }
3967 }
3968 BuiltinId::Zip => {
3969 if args.len() < 2 {
3970 return Err(runtime_err("zip() expects two lists"));
3971 }
3972 if let (VmValue::List(a), VmValue::List(b)) = (&args[0], &args[1]) {
3973 let pairs: Vec<VmValue> = a
3974 .iter()
3975 .zip(b.iter())
3976 .map(|(x, y)| VmValue::List(Box::new(vec![x.clone(), y.clone()])))
3977 .collect();
3978 Ok(VmValue::List(Box::new(pairs)))
3979 } else {
3980 Err(runtime_err("zip() expects two lists"))
3981 }
3982 }
3983 BuiltinId::Enumerate => {
3984 if args.is_empty() {
3985 return Err(runtime_err("enumerate() expects a list"));
3986 }
3987 if let VmValue::List(items) = &args[0] {
3988 let pairs: Vec<VmValue> = items
3989 .iter()
3990 .enumerate()
3991 .map(|(i, v)| {
3992 VmValue::List(Box::new(vec![VmValue::Int(i as i64), v.clone()]))
3993 })
3994 .collect();
3995 Ok(VmValue::List(Box::new(pairs)))
3996 } else {
3997 Err(runtime_err("enumerate() expects a list"))
3998 }
3999 }
4000 BuiltinId::Bool => {
4001 if args.is_empty() {
4002 return Err(runtime_err("bool() expects a value"));
4003 }
4004 Ok(VmValue::Bool(args[0].is_truthy()))
4005 }
4006
4007 #[cfg(feature = "native")]
4009 BuiltinId::Spawn => {
4010 if args.is_empty() {
4011 return Err(runtime_err("spawn() expects a function argument"));
4012 }
4013 match &args[0] {
4014 VmValue::Function(closure) => {
4015 let proto = closure.prototype.clone();
4016 let mut closed_upvalues = Vec::new();
4018 for uv in &closure.upvalues {
4019 match uv {
4020 UpvalueRef::Open { stack_index } => {
4021 let val = self.stack[*stack_index].clone();
4022 closed_upvalues.push(UpvalueRef::Closed(val));
4023 }
4024 UpvalueRef::Closed(v) => {
4025 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4026 }
4027 }
4028 }
4029 let globals = self.globals.clone();
4030 let (tx, rx) = mpsc::channel::<Result<VmValue, String>>();
4031
4032 std::thread::spawn(move || {
4033 let mut vm = Vm::new();
4034 vm.globals = globals;
4035 let result = vm.execute_closure(&proto, &closed_upvalues);
4036 let _ = tx.send(result.map_err(|e| match e {
4037 TlError::Runtime(re) => re.message,
4038 other => format!("{other}"),
4039 }));
4040 });
4041
4042 Ok(VmValue::Task(Arc::new(VmTask::new(rx))))
4043 }
4044 _ => Err(runtime_err("spawn() expects a function")),
4045 }
4046 }
4047 #[cfg(feature = "native")]
4048 BuiltinId::Sleep => {
4049 if args.is_empty() {
4050 return Err(runtime_err("sleep() expects a duration in milliseconds"));
4051 }
4052 match &args[0] {
4053 VmValue::Int(ms) => {
4054 std::thread::sleep(Duration::from_millis(*ms as u64));
4055 Ok(VmValue::None)
4056 }
4057 _ => Err(runtime_err("sleep() expects an integer (milliseconds)")),
4058 }
4059 }
4060 #[cfg(feature = "native")]
4061 BuiltinId::Channel => {
4062 let capacity = match args.first() {
4063 Some(VmValue::Int(n)) => *n as usize,
4064 None => 64,
4065 _ => {
4066 return Err(runtime_err(
4067 "channel() expects an optional integer capacity",
4068 ));
4069 }
4070 };
4071 Ok(VmValue::Channel(Arc::new(VmChannel::new(capacity))))
4072 }
4073 #[cfg(feature = "native")]
4074 BuiltinId::Send => {
4075 if args.len() < 2 {
4076 return Err(runtime_err("send() expects a channel and a value"));
4077 }
4078 match &args[0] {
4079 VmValue::Channel(ch) => {
4080 ch.sender
4081 .send(args[1].clone())
4082 .map_err(|_| runtime_err("Channel disconnected"))?;
4083 Ok(VmValue::None)
4084 }
4085 _ => Err(runtime_err("send() expects a channel as first argument")),
4086 }
4087 }
4088 #[cfg(feature = "native")]
4089 BuiltinId::Recv => {
4090 if args.is_empty() {
4091 return Err(runtime_err("recv() expects a channel"));
4092 }
4093 match &args[0] {
4094 VmValue::Channel(ch) => {
4095 let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4096 match guard.recv() {
4097 Ok(val) => Ok(val),
4098 Err(_) => Ok(VmValue::None),
4099 }
4100 }
4101 _ => Err(runtime_err("recv() expects a channel")),
4102 }
4103 }
4104 #[cfg(feature = "native")]
4105 BuiltinId::TryRecv => {
4106 if args.is_empty() {
4107 return Err(runtime_err("try_recv() expects a channel"));
4108 }
4109 match &args[0] {
4110 VmValue::Channel(ch) => {
4111 let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4112 match guard.try_recv() {
4113 Ok(val) => Ok(val),
4114 Err(_) => Ok(VmValue::None),
4115 }
4116 }
4117 _ => Err(runtime_err("try_recv() expects a channel")),
4118 }
4119 }
4120 #[cfg(feature = "native")]
4121 BuiltinId::AwaitAll => {
4122 if args.is_empty() {
4123 return Err(runtime_err("await_all() expects a list of tasks"));
4124 }
4125 match &args[0] {
4126 VmValue::List(tasks) => {
4127 let mut results = Vec::with_capacity(tasks.len());
4128 for task in tasks.iter() {
4129 match task {
4130 VmValue::Task(t) => {
4131 let rx = {
4132 let mut guard =
4133 t.receiver.lock().unwrap_or_else(|e| e.into_inner());
4134 guard.take()
4135 };
4136 match rx {
4137 Some(receiver) => match receiver.recv() {
4138 Ok(Ok(val)) => results.push(val),
4139 Ok(Err(e)) => return Err(runtime_err(e)),
4140 Err(_) => {
4141 return Err(runtime_err(
4142 "Task channel disconnected",
4143 ));
4144 }
4145 },
4146 None => return Err(runtime_err("Task already awaited")),
4147 }
4148 }
4149 other => results.push(other.clone()),
4150 }
4151 }
4152 Ok(VmValue::List(Box::new(results)))
4153 }
4154 _ => Err(runtime_err("await_all() expects a list")),
4155 }
4156 }
4157 #[cfg(feature = "native")]
4158 BuiltinId::Pmap => {
4159 if args.len() < 2 {
4160 return Err(runtime_err("pmap() expects a list and a function"));
4161 }
4162 let items = match &args[0] {
4163 VmValue::List(items) => (**items).clone(),
4164 _ => return Err(runtime_err("pmap() expects a list as first argument")),
4165 };
4166 let closure = match &args[1] {
4167 VmValue::Function(c) => c.clone(),
4168 _ => return Err(runtime_err("pmap() expects a function as second argument")),
4169 };
4170
4171 let mut closed_upvalues = Vec::new();
4173 for uv in &closure.upvalues {
4174 match uv {
4175 UpvalueRef::Open { stack_index } => {
4176 let val = self.stack[*stack_index].clone();
4177 closed_upvalues.push(UpvalueRef::Closed(val));
4178 }
4179 UpvalueRef::Closed(v) => {
4180 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4181 }
4182 }
4183 }
4184
4185 let proto = closure.prototype.clone();
4186 let globals = self.globals.clone();
4187
4188 let mut handles = Vec::with_capacity(items.len());
4190 for item in items {
4191 let proto = proto.clone();
4192 let upvalues = closed_upvalues.clone();
4193 let globals = globals.clone();
4194 let handle = std::thread::spawn(move || {
4195 let mut vm = Vm::new();
4196 vm.globals = globals;
4197 vm.execute_closure_with_args(&proto, &upvalues, &[item])
4198 .map_err(|e| match e {
4199 TlError::Runtime(re) => re.message,
4200 other => format!("{other}"),
4201 })
4202 });
4203 handles.push(handle);
4204 }
4205
4206 let mut results = Vec::with_capacity(handles.len());
4207 for handle in handles {
4208 match handle.join() {
4209 Ok(Ok(val)) => results.push(val),
4210 Ok(Err(e)) => return Err(runtime_err(e)),
4211 Err(_) => return Err(runtime_err("pmap() thread panicked")),
4212 }
4213 }
4214 Ok(VmValue::List(Box::new(results)))
4215 }
4216 #[cfg(feature = "native")]
4217 BuiltinId::Timeout => {
4218 if args.len() < 2 {
4219 return Err(runtime_err(
4220 "timeout() expects a task and a duration in milliseconds",
4221 ));
4222 }
4223 let ms = match &args[1] {
4224 VmValue::Int(n) => *n as u64,
4225 _ => return Err(runtime_err("timeout() expects an integer duration")),
4226 };
4227 match &args[0] {
4228 VmValue::Task(task) => {
4229 let rx = {
4230 let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
4231 guard.take()
4232 };
4233 match rx {
4234 Some(receiver) => {
4235 match receiver.recv_timeout(Duration::from_millis(ms)) {
4236 Ok(Ok(val)) => Ok(val),
4237 Ok(Err(e)) => Err(runtime_err(e)),
4238 Err(mpsc::RecvTimeoutError::Timeout) => {
4239 Err(runtime_err("Task timed out"))
4240 }
4241 Err(mpsc::RecvTimeoutError::Disconnected) => {
4242 Err(runtime_err("Task channel disconnected"))
4243 }
4244 }
4245 }
4246 None => Err(runtime_err("Task already awaited")),
4247 }
4248 }
4249 _ => Err(runtime_err("timeout() expects a task as first argument")),
4250 }
4251 }
4252 #[cfg(not(feature = "native"))]
4253 BuiltinId::Spawn
4254 | BuiltinId::Sleep
4255 | BuiltinId::Channel
4256 | BuiltinId::Send
4257 | BuiltinId::Recv
4258 | BuiltinId::TryRecv
4259 | BuiltinId::AwaitAll
4260 | BuiltinId::Pmap
4261 | BuiltinId::Timeout => Err(runtime_err("Threading not available in WASM")),
4262 BuiltinId::Next => {
4264 if args.is_empty() {
4265 return Err(runtime_err("next() expects a generator"));
4266 }
4267 match &args[0] {
4268 VmValue::Generator(gen_arc) => {
4269 let g = gen_arc.clone();
4270 self.generator_next(&g)
4271 }
4272 _ => Err(runtime_err("next() expects a generator")),
4273 }
4274 }
4275 BuiltinId::IsGenerator => {
4276 let val = args.first().unwrap_or(&VmValue::None);
4277 Ok(VmValue::Bool(matches!(val, VmValue::Generator(_))))
4278 }
4279 BuiltinId::Iter => {
4280 if args.is_empty() {
4281 return Err(runtime_err("iter() expects a list"));
4282 }
4283 match &args[0] {
4284 VmValue::List(items) => {
4285 let gn = VmGenerator::new(GeneratorKind::ListIter {
4286 items: (**items).clone(),
4287 index: 0,
4288 });
4289 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4290 }
4291 _ => Err(runtime_err("iter() expects a list")),
4292 }
4293 }
4294 BuiltinId::Take => {
4295 if args.len() < 2 {
4296 return Err(runtime_err("take() expects a generator and a count"));
4297 }
4298 let gen_arc = match &args[0] {
4299 VmValue::Generator(g) => g.clone(),
4300 _ => return Err(runtime_err("take() expects a generator as first argument")),
4301 };
4302 let n = match &args[1] {
4303 VmValue::Int(n) => *n as usize,
4304 _ => return Err(runtime_err("take() expects an integer count")),
4305 };
4306 let gn = VmGenerator::new(GeneratorKind::Take {
4307 source: gen_arc,
4308 remaining: n,
4309 });
4310 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4311 }
4312 BuiltinId::Skip_ => {
4313 if args.len() < 2 {
4314 return Err(runtime_err("skip() expects a generator and a count"));
4315 }
4316 let gen_arc = match &args[0] {
4317 VmValue::Generator(g) => g.clone(),
4318 _ => return Err(runtime_err("skip() expects a generator as first argument")),
4319 };
4320 let n = match &args[1] {
4321 VmValue::Int(n) => *n as usize,
4322 _ => return Err(runtime_err("skip() expects an integer count")),
4323 };
4324 let gn = VmGenerator::new(GeneratorKind::Skip {
4325 source: gen_arc,
4326 remaining: n,
4327 });
4328 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4329 }
4330 BuiltinId::GenCollect => {
4331 if args.is_empty() {
4332 return Err(runtime_err("gen_collect() expects a generator"));
4333 }
4334 match &args[0] {
4335 VmValue::Generator(gen_arc) => {
4336 let g = gen_arc.clone();
4337 let mut items = Vec::new();
4338 loop {
4339 let val = self.generator_next(&g)?;
4340 if matches!(val, VmValue::None) {
4341 break;
4342 }
4343 items.push(val);
4344 }
4345 Ok(VmValue::List(Box::new(items)))
4346 }
4347 _ => Err(runtime_err("gen_collect() expects a generator")),
4348 }
4349 }
4350 BuiltinId::GenMap => {
4351 if args.len() < 2 {
4352 return Err(runtime_err("gen_map() expects a generator and a function"));
4353 }
4354 let gen_arc = match &args[0] {
4355 VmValue::Generator(g) => g.clone(),
4356 _ => {
4357 return Err(runtime_err(
4358 "gen_map() expects a generator as first argument",
4359 ));
4360 }
4361 };
4362 let func = args[1].clone();
4363 let gn = VmGenerator::new(GeneratorKind::Map {
4364 source: gen_arc,
4365 func,
4366 });
4367 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4368 }
4369 BuiltinId::GenFilter => {
4370 if args.len() < 2 {
4371 return Err(runtime_err(
4372 "gen_filter() expects a generator and a function",
4373 ));
4374 }
4375 let gen_arc = match &args[0] {
4376 VmValue::Generator(g) => g.clone(),
4377 _ => {
4378 return Err(runtime_err(
4379 "gen_filter() expects a generator as first argument",
4380 ));
4381 }
4382 };
4383 let func = args[1].clone();
4384 let gn = VmGenerator::new(GeneratorKind::Filter {
4385 source: gen_arc,
4386 func,
4387 });
4388 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4389 }
4390 BuiltinId::Chain => {
4391 if args.len() < 2 {
4392 return Err(runtime_err("chain() expects two generators"));
4393 }
4394 let first = match &args[0] {
4395 VmValue::Generator(g) => g.clone(),
4396 _ => return Err(runtime_err("chain() expects generators")),
4397 };
4398 let second = match &args[1] {
4399 VmValue::Generator(g) => g.clone(),
4400 _ => return Err(runtime_err("chain() expects generators")),
4401 };
4402 let gn = VmGenerator::new(GeneratorKind::Chain {
4403 first,
4404 second,
4405 on_second: false,
4406 });
4407 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4408 }
4409 BuiltinId::GenZip => {
4410 if args.len() < 2 {
4411 return Err(runtime_err("gen_zip() expects two generators"));
4412 }
4413 let first = match &args[0] {
4414 VmValue::Generator(g) => g.clone(),
4415 _ => return Err(runtime_err("gen_zip() expects generators")),
4416 };
4417 let second = match &args[1] {
4418 VmValue::Generator(g) => g.clone(),
4419 _ => return Err(runtime_err("gen_zip() expects generators")),
4420 };
4421 let gn = VmGenerator::new(GeneratorKind::Zip { first, second });
4422 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4423 }
4424 BuiltinId::GenEnumerate => {
4425 if args.is_empty() {
4426 return Err(runtime_err("gen_enumerate() expects a generator"));
4427 }
4428 let gen_arc = match &args[0] {
4429 VmValue::Generator(g) => g.clone(),
4430 _ => return Err(runtime_err("gen_enumerate() expects a generator")),
4431 };
4432 let gn = VmGenerator::new(GeneratorKind::Enumerate {
4433 source: gen_arc,
4434 index: 0,
4435 });
4436 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4437 }
4438 BuiltinId::Ok => {
4440 let val = if args.is_empty() {
4441 VmValue::None
4442 } else {
4443 args[0].clone()
4444 };
4445 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4446 type_name: Arc::from("Result"),
4447 variant: Arc::from("Ok"),
4448 fields: vec![val],
4449 })))
4450 }
4451 BuiltinId::Err_ => {
4452 let val = if args.is_empty() {
4453 VmValue::String(Arc::from("error"))
4454 } else {
4455 args[0].clone()
4456 };
4457 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4458 type_name: Arc::from("Result"),
4459 variant: Arc::from("Err"),
4460 fields: vec![val],
4461 })))
4462 }
4463 BuiltinId::IsOk => {
4464 if args.is_empty() {
4465 return Err(runtime_err("is_ok() expects an argument"));
4466 }
4467 match &args[0] {
4468 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4469 Ok(VmValue::Bool(ei.variant.as_ref() == "Ok"))
4470 }
4471 _ => Ok(VmValue::Bool(false)),
4472 }
4473 }
4474 BuiltinId::IsErr => {
4475 if args.is_empty() {
4476 return Err(runtime_err("is_err() expects an argument"));
4477 }
4478 match &args[0] {
4479 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4480 Ok(VmValue::Bool(ei.variant.as_ref() == "Err"))
4481 }
4482 _ => Ok(VmValue::Bool(false)),
4483 }
4484 }
4485 BuiltinId::Unwrap => {
4486 if args.is_empty() {
4487 return Err(runtime_err("unwrap() expects an argument"));
4488 }
4489 match &args[0] {
4490 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4491 if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
4492 Ok(ei.fields[0].clone())
4493 } else if ei.variant.as_ref() == "Err" {
4494 let msg = if ei.fields.is_empty() {
4495 "error".to_string()
4496 } else {
4497 format!("{}", ei.fields[0])
4498 };
4499 Err(runtime_err(format!("unwrap() called on Err({msg})")))
4500 } else {
4501 Ok(VmValue::None)
4502 }
4503 }
4504 VmValue::None => Err(runtime_err("unwrap() called on none".to_string())),
4505 other => Ok(other.clone()),
4506 }
4507 }
4508 BuiltinId::SetFrom => {
4509 let list = match args.first() {
4510 Some(VmValue::List(items)) => items,
4511 _ => return Err(runtime_err("set_from() expects a list")),
4512 };
4513 if list.is_empty() {
4514 return Ok(VmValue::Set(Box::default()));
4515 }
4516 let mut result = Vec::new();
4517 for item in list.iter() {
4518 if !result.iter().any(|x| vm_values_equal(x, item)) {
4519 result.push(item.clone());
4520 }
4521 }
4522 Ok(VmValue::Set(Box::new(result)))
4523 }
4524 BuiltinId::SetAdd => {
4525 if args.len() < 2 {
4526 return Err(runtime_err("set_add() expects 2 arguments"));
4527 }
4528 let val = &args[1];
4529 match &args[0] {
4530 VmValue::Set(items) => {
4531 let mut new_items = items.clone();
4532 if !new_items.iter().any(|x| vm_values_equal(x, val)) {
4533 new_items.push(val.clone());
4534 }
4535 Ok(VmValue::Set(new_items))
4536 }
4537 _ => Err(runtime_err("set_add() first argument must be a set")),
4538 }
4539 }
4540 BuiltinId::SetRemove => {
4541 if args.len() < 2 {
4542 return Err(runtime_err("set_remove() expects 2 arguments"));
4543 }
4544 let val = &args[1];
4545 match &args[0] {
4546 VmValue::Set(items) => {
4547 let new_items: Vec<VmValue> = items
4548 .iter()
4549 .filter(|x| !vm_values_equal(x, val))
4550 .cloned()
4551 .collect();
4552 Ok(VmValue::Set(Box::new(new_items)))
4553 }
4554 _ => Err(runtime_err("set_remove() first argument must be a set")),
4555 }
4556 }
4557 BuiltinId::SetContains => {
4558 if args.len() < 2 {
4559 return Err(runtime_err("set_contains() expects 2 arguments"));
4560 }
4561 let val = &args[1];
4562 match &args[0] {
4563 VmValue::Set(items) => {
4564 Ok(VmValue::Bool(items.iter().any(|x| vm_values_equal(x, val))))
4565 }
4566 _ => Err(runtime_err("set_contains() first argument must be a set")),
4567 }
4568 }
4569 BuiltinId::SetUnion => {
4570 if args.len() < 2 {
4571 return Err(runtime_err("set_union() expects 2 arguments"));
4572 }
4573 match (&args[0], &args[1]) {
4574 (VmValue::Set(a), VmValue::Set(b)) => {
4575 let mut result = a.clone();
4576 for item in b.iter() {
4577 if !result.iter().any(|x| vm_values_equal(x, item)) {
4578 result.push(item.clone());
4579 }
4580 }
4581 Ok(VmValue::Set(result))
4582 }
4583 _ => Err(runtime_err("set_union() expects two sets")),
4584 }
4585 }
4586 BuiltinId::SetIntersection => {
4587 if args.len() < 2 {
4588 return Err(runtime_err("set_intersection() expects 2 arguments"));
4589 }
4590 match (&args[0], &args[1]) {
4591 (VmValue::Set(a), VmValue::Set(b)) => {
4592 let result: Vec<VmValue> = a
4593 .iter()
4594 .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
4595 .cloned()
4596 .collect();
4597 Ok(VmValue::Set(Box::new(result)))
4598 }
4599 _ => Err(runtime_err("set_intersection() expects two sets")),
4600 }
4601 }
4602 BuiltinId::SetDifference => {
4603 if args.len() < 2 {
4604 return Err(runtime_err("set_difference() expects 2 arguments"));
4605 }
4606 match (&args[0], &args[1]) {
4607 (VmValue::Set(a), VmValue::Set(b)) => {
4608 let result: Vec<VmValue> = a
4609 .iter()
4610 .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
4611 .cloned()
4612 .collect();
4613 Ok(VmValue::Set(Box::new(result)))
4614 }
4615 _ => Err(runtime_err("set_difference() expects two sets")),
4616 }
4617 }
4618
4619 #[cfg(feature = "native")]
4621 BuiltinId::FillNull => {
4622 if args.len() < 2 {
4623 return Err(runtime_err(
4624 "fill_null() expects (table, column, [strategy], [value])",
4625 ));
4626 }
4627 let df = match &args[0] {
4628 VmValue::Table(t) => t.df.clone(),
4629 _ => return Err(runtime_err("fill_null() first arg must be a table")),
4630 };
4631 let column = match &args[1] {
4632 VmValue::String(s) => s.to_string(),
4633 _ => return Err(runtime_err("fill_null() column must be a string")),
4634 };
4635 let strategy = if args.len() > 2 {
4636 match &args[2] {
4637 VmValue::String(s) => s.to_string(),
4638 _ => "value".to_string(),
4639 }
4640 } else {
4641 "value".to_string()
4642 };
4643 let fill_value = if args.len() > 3 {
4644 match &args[3] {
4645 VmValue::Int(n) => Some(*n as f64),
4646 VmValue::Float(f) => Some(*f),
4647 _ => None,
4648 }
4649 } else if args.len() > 2 && strategy == "value" {
4650 match &args[2] {
4651 VmValue::Int(n) => {
4652 return Ok(VmValue::Table(VmTable {
4653 df: self
4654 .engine()
4655 .fill_null(df, &column, "value", Some(*n as f64))
4656 .map_err(runtime_err)?,
4657 }));
4658 }
4659 VmValue::Float(f) => {
4660 return Ok(VmValue::Table(VmTable {
4661 df: self
4662 .engine()
4663 .fill_null(df, &column, "value", Some(*f))
4664 .map_err(runtime_err)?,
4665 }));
4666 }
4667 _ => None,
4668 }
4669 } else {
4670 None
4671 };
4672 let result = self
4673 .engine()
4674 .fill_null(df, &column, &strategy, fill_value)
4675 .map_err(runtime_err)?;
4676 Ok(VmValue::Table(VmTable { df: result }))
4677 }
4678 #[cfg(feature = "native")]
4679 BuiltinId::DropNull => {
4680 if args.len() < 2 {
4681 return Err(runtime_err("drop_null() expects (table, column)"));
4682 }
4683 let df = match &args[0] {
4684 VmValue::Table(t) => t.df.clone(),
4685 _ => return Err(runtime_err("drop_null() first arg must be a table")),
4686 };
4687 let column = match &args[1] {
4688 VmValue::String(s) => s.to_string(),
4689 _ => return Err(runtime_err("drop_null() column must be a string")),
4690 };
4691 let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
4692 Ok(VmValue::Table(VmTable { df: result }))
4693 }
4694 #[cfg(feature = "native")]
4695 BuiltinId::Dedup => {
4696 if args.is_empty() {
4697 return Err(runtime_err("dedup() expects (table, [columns...])"));
4698 }
4699 let df = match &args[0] {
4700 VmValue::Table(t) => t.df.clone(),
4701 _ => return Err(runtime_err("dedup() first arg must be a table")),
4702 };
4703 let columns: Vec<String> = args[1..]
4704 .iter()
4705 .filter_map(|a| {
4706 if let VmValue::String(s) = a {
4707 Some(s.to_string())
4708 } else {
4709 None
4710 }
4711 })
4712 .collect();
4713 let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
4714 Ok(VmValue::Table(VmTable { df: result }))
4715 }
4716 #[cfg(feature = "native")]
4717 BuiltinId::Clamp => {
4718 if args.len() < 4 {
4719 return Err(runtime_err("clamp() expects (table, column, min, max)"));
4720 }
4721 let df = match &args[0] {
4722 VmValue::Table(t) => t.df.clone(),
4723 _ => return Err(runtime_err("clamp() first arg must be a table")),
4724 };
4725 let column = match &args[1] {
4726 VmValue::String(s) => s.to_string(),
4727 _ => return Err(runtime_err("clamp() column must be a string")),
4728 };
4729 let min_val = match &args[2] {
4730 VmValue::Int(n) => *n as f64,
4731 VmValue::Float(f) => *f,
4732 _ => return Err(runtime_err("clamp() min must be a number")),
4733 };
4734 let max_val = match &args[3] {
4735 VmValue::Int(n) => *n as f64,
4736 VmValue::Float(f) => *f,
4737 _ => return Err(runtime_err("clamp() max must be a number")),
4738 };
4739 let result = self
4740 .engine()
4741 .clamp(df, &column, min_val, max_val)
4742 .map_err(runtime_err)?;
4743 Ok(VmValue::Table(VmTable { df: result }))
4744 }
4745 #[cfg(feature = "native")]
4746 BuiltinId::DataProfile => {
4747 if args.is_empty() {
4748 return Err(runtime_err("data_profile() expects (table)"));
4749 }
4750 let df = match &args[0] {
4751 VmValue::Table(t) => t.df.clone(),
4752 _ => return Err(runtime_err("data_profile() arg must be a table")),
4753 };
4754 let result = self.engine().data_profile(df).map_err(runtime_err)?;
4755 Ok(VmValue::Table(VmTable { df: result }))
4756 }
4757 #[cfg(feature = "native")]
4758 BuiltinId::RowCount => {
4759 if args.is_empty() {
4760 return Err(runtime_err("row_count() expects (table)"));
4761 }
4762 let df = match &args[0] {
4763 VmValue::Table(t) => t.df.clone(),
4764 _ => return Err(runtime_err("row_count() arg must be a table")),
4765 };
4766 let count = self.engine().row_count(df).map_err(runtime_err)?;
4767 Ok(VmValue::Int(count))
4768 }
4769 #[cfg(feature = "native")]
4770 BuiltinId::NullRate => {
4771 if args.len() < 2 {
4772 return Err(runtime_err("null_rate() expects (table, column)"));
4773 }
4774 let df = match &args[0] {
4775 VmValue::Table(t) => t.df.clone(),
4776 _ => return Err(runtime_err("null_rate() first arg must be a table")),
4777 };
4778 let column = match &args[1] {
4779 VmValue::String(s) => s.to_string(),
4780 _ => return Err(runtime_err("null_rate() column must be a string")),
4781 };
4782 let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
4783 Ok(VmValue::Float(rate))
4784 }
4785 #[cfg(feature = "native")]
4786 BuiltinId::IsUnique => {
4787 if args.len() < 2 {
4788 return Err(runtime_err("is_unique() expects (table, column)"));
4789 }
4790 let df = match &args[0] {
4791 VmValue::Table(t) => t.df.clone(),
4792 _ => return Err(runtime_err("is_unique() first arg must be a table")),
4793 };
4794 let column = match &args[1] {
4795 VmValue::String(s) => s.to_string(),
4796 _ => return Err(runtime_err("is_unique() column must be a string")),
4797 };
4798 let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
4799 Ok(VmValue::Bool(unique))
4800 }
4801 #[cfg(not(feature = "native"))]
4802 BuiltinId::FillNull
4803 | BuiltinId::DropNull
4804 | BuiltinId::Dedup
4805 | BuiltinId::Clamp
4806 | BuiltinId::DataProfile
4807 | BuiltinId::RowCount
4808 | BuiltinId::NullRate
4809 | BuiltinId::IsUnique => Err(runtime_err("Data operations not available in WASM")),
4810 #[cfg(feature = "native")]
4811 BuiltinId::IsEmail => {
4812 if args.is_empty() {
4813 return Err(runtime_err("is_email() expects 1 argument"));
4814 }
4815 let s = match &args[0] {
4816 VmValue::String(s) => s.to_string(),
4817 _ => return Err(runtime_err("is_email() arg must be a string")),
4818 };
4819 Ok(VmValue::Bool(tl_data::validate::is_email(&s)))
4820 }
4821 #[cfg(feature = "native")]
4822 BuiltinId::IsUrl => {
4823 if args.is_empty() {
4824 return Err(runtime_err("is_url() expects 1 argument"));
4825 }
4826 let s = match &args[0] {
4827 VmValue::String(s) => s.to_string(),
4828 _ => return Err(runtime_err("is_url() arg must be a string")),
4829 };
4830 Ok(VmValue::Bool(tl_data::validate::is_url(&s)))
4831 }
4832 #[cfg(feature = "native")]
4833 BuiltinId::IsPhone => {
4834 if args.is_empty() {
4835 return Err(runtime_err("is_phone() expects 1 argument"));
4836 }
4837 let s = match &args[0] {
4838 VmValue::String(s) => s.to_string(),
4839 _ => return Err(runtime_err("is_phone() arg must be a string")),
4840 };
4841 Ok(VmValue::Bool(tl_data::validate::is_phone(&s)))
4842 }
4843 #[cfg(feature = "native")]
4844 BuiltinId::IsBetween => {
4845 if args.len() < 3 {
4846 return Err(runtime_err("is_between() expects (value, low, high)"));
4847 }
4848 let val = match &args[0] {
4849 VmValue::Int(n) => *n as f64,
4850 VmValue::Float(f) => *f,
4851 _ => return Err(runtime_err("is_between() value must be a number")),
4852 };
4853 let low = match &args[1] {
4854 VmValue::Int(n) => *n as f64,
4855 VmValue::Float(f) => *f,
4856 _ => return Err(runtime_err("is_between() low must be a number")),
4857 };
4858 let high = match &args[2] {
4859 VmValue::Int(n) => *n as f64,
4860 VmValue::Float(f) => *f,
4861 _ => return Err(runtime_err("is_between() high must be a number")),
4862 };
4863 Ok(VmValue::Bool(tl_data::validate::is_between(val, low, high)))
4864 }
4865 #[cfg(feature = "native")]
4866 BuiltinId::Levenshtein => {
4867 if args.len() < 2 {
4868 return Err(runtime_err("levenshtein() expects (str_a, str_b)"));
4869 }
4870 let a = match &args[0] {
4871 VmValue::String(s) => s.to_string(),
4872 _ => return Err(runtime_err("levenshtein() args must be strings")),
4873 };
4874 let b = match &args[1] {
4875 VmValue::String(s) => s.to_string(),
4876 _ => return Err(runtime_err("levenshtein() args must be strings")),
4877 };
4878 Ok(VmValue::Int(tl_data::validate::levenshtein(&a, &b) as i64))
4879 }
4880 #[cfg(feature = "native")]
4881 BuiltinId::Soundex => {
4882 if args.is_empty() {
4883 return Err(runtime_err("soundex() expects 1 argument"));
4884 }
4885 let s = match &args[0] {
4886 VmValue::String(s) => s.to_string(),
4887 _ => return Err(runtime_err("soundex() arg must be a string")),
4888 };
4889 Ok(VmValue::String(Arc::from(
4890 tl_data::validate::soundex(&s).as_str(),
4891 )))
4892 }
4893 #[cfg(not(feature = "native"))]
4894 BuiltinId::IsEmail
4895 | BuiltinId::IsUrl
4896 | BuiltinId::IsPhone
4897 | BuiltinId::IsBetween
4898 | BuiltinId::Levenshtein
4899 | BuiltinId::Soundex => Err(runtime_err("Data validation not available in WASM")),
4900 #[cfg(feature = "native")]
4901 BuiltinId::ReadMysql => {
4902 #[cfg(feature = "mysql")]
4903 {
4904 if args.len() < 2 {
4905 return Err(runtime_err("read_mysql() expects (conn_str, query)"));
4906 }
4907 let conn_str = match &args[0] {
4908 VmValue::String(s) => s.to_string(),
4909 _ => return Err(runtime_err("read_mysql() conn_str must be a string")),
4910 };
4911 let query = match &args[1] {
4912 VmValue::String(s) => s.to_string(),
4913 _ => return Err(runtime_err("read_mysql() query must be a string")),
4914 };
4915 let df = self
4916 .engine()
4917 .read_mysql(&conn_str, &query)
4918 .map_err(runtime_err)?;
4919 Ok(VmValue::Table(VmTable { df }))
4920 }
4921 #[cfg(not(feature = "mysql"))]
4922 Err(runtime_err("read_mysql() requires the 'mysql' feature"))
4923 }
4924 #[cfg(feature = "native")]
4925 BuiltinId::ReadSqlite => {
4926 #[cfg(feature = "sqlite")]
4927 {
4928 if args.len() < 2 {
4929 return Err(runtime_err("read_sqlite() expects (db_path, query)"));
4930 }
4931 let db_path = match &args[0] {
4932 VmValue::String(s) => s.to_string(),
4933 _ => return Err(runtime_err("read_sqlite() db_path must be a string")),
4934 };
4935 let query = match &args[1] {
4936 VmValue::String(s) => s.to_string(),
4937 _ => return Err(runtime_err("read_sqlite() query must be a string")),
4938 };
4939 let df = self
4940 .engine()
4941 .read_sqlite(&db_path, &query)
4942 .map_err(runtime_err)?;
4943 Ok(VmValue::Table(VmTable { df }))
4944 }
4945 #[cfg(not(feature = "sqlite"))]
4946 Err(runtime_err("read_sqlite() requires the 'sqlite' feature"))
4947 }
4948 #[cfg(feature = "native")]
4949 BuiltinId::WriteSqlite => {
4950 #[cfg(feature = "sqlite")]
4951 {
4952 if args.len() < 3 {
4953 return Err(runtime_err(
4954 "write_sqlite() expects (table, db_path, table_name)",
4955 ));
4956 }
4957 let df = match &args[0] {
4958 VmValue::Table(t) => t.df.clone(),
4959 _ => return Err(runtime_err("write_sqlite() first arg must be a table")),
4960 };
4961 let db_path = match &args[1] {
4962 VmValue::String(s) => s.to_string(),
4963 _ => return Err(runtime_err("write_sqlite() db_path must be a string")),
4964 };
4965 let table_name = match &args[2] {
4966 VmValue::String(s) => s.to_string(),
4967 _ => return Err(runtime_err("write_sqlite() table_name must be a string")),
4968 };
4969 self.engine()
4970 .write_sqlite(df, &db_path, &table_name)
4971 .map_err(runtime_err)?;
4972 Ok(VmValue::None)
4973 }
4974 #[cfg(not(feature = "sqlite"))]
4975 Err(runtime_err("write_sqlite() requires the 'sqlite' feature"))
4976 }
4977 #[cfg(feature = "native")]
4978 BuiltinId::ReadDuckDb => {
4979 #[cfg(feature = "duckdb")]
4980 {
4981 if args.len() < 2 {
4982 return Err(runtime_err("duckdb() expects (db_path, query)"));
4983 }
4984 let db_path = match &args[0] {
4985 VmValue::String(s) => s.to_string(),
4986 _ => return Err(runtime_err("duckdb() db_path must be a string")),
4987 };
4988 let query = match &args[1] {
4989 VmValue::String(s) => s.to_string(),
4990 _ => return Err(runtime_err("duckdb() query must be a string")),
4991 };
4992 let df = self
4993 .engine()
4994 .read_duckdb(&db_path, &query)
4995 .map_err(runtime_err)?;
4996 Ok(VmValue::Table(VmTable { df }))
4997 }
4998 #[cfg(not(feature = "duckdb"))]
4999 Err(runtime_err("duckdb() requires the 'duckdb' feature"))
5000 }
5001 #[cfg(feature = "native")]
5002 BuiltinId::WriteDuckDb => {
5003 #[cfg(feature = "duckdb")]
5004 {
5005 if args.len() < 3 {
5006 return Err(runtime_err(
5007 "write_duckdb() expects (table, db_path, table_name)",
5008 ));
5009 }
5010 let df = match &args[0] {
5011 VmValue::Table(t) => t.df.clone(),
5012 _ => return Err(runtime_err("write_duckdb() first arg must be a table")),
5013 };
5014 let db_path = match &args[1] {
5015 VmValue::String(s) => s.to_string(),
5016 _ => return Err(runtime_err("write_duckdb() db_path must be a string")),
5017 };
5018 let table_name = match &args[2] {
5019 VmValue::String(s) => s.to_string(),
5020 _ => return Err(runtime_err("write_duckdb() table_name must be a string")),
5021 };
5022 self.engine()
5023 .write_duckdb(df, &db_path, &table_name)
5024 .map_err(runtime_err)?;
5025 Ok(VmValue::None)
5026 }
5027 #[cfg(not(feature = "duckdb"))]
5028 Err(runtime_err("write_duckdb() requires the 'duckdb' feature"))
5029 }
5030 #[cfg(feature = "native")]
5031 BuiltinId::ReadRedshift => {
5032 if args.len() < 2 {
5033 return Err(runtime_err("redshift() expects (conn_str, query)"));
5034 }
5035 let conn_str = match &args[0] {
5036 VmValue::String(s) => {
5037 let s_str = s.to_string();
5038 resolve_tl_config_connection(&s_str)
5039 }
5040 _ => return Err(runtime_err("redshift() conn_str must be a string")),
5041 };
5042 let query = match &args[1] {
5043 VmValue::String(s) => s.to_string(),
5044 _ => return Err(runtime_err("redshift() query must be a string")),
5045 };
5046 let df = self
5047 .engine()
5048 .read_redshift(&conn_str, &query)
5049 .map_err(runtime_err)?;
5050 Ok(VmValue::Table(VmTable { df }))
5051 }
5052 #[cfg(feature = "native")]
5053 BuiltinId::ReadMssql => {
5054 #[cfg(feature = "mssql")]
5055 {
5056 if args.len() < 2 {
5057 return Err(runtime_err("mssql() expects (conn_str, query)"));
5058 }
5059 let conn_str = match &args[0] {
5060 VmValue::String(s) => {
5061 let s_str = s.to_string();
5062 resolve_tl_config_connection(&s_str)
5063 }
5064 _ => return Err(runtime_err("mssql() conn_str must be a string")),
5065 };
5066 let query = match &args[1] {
5067 VmValue::String(s) => s.to_string(),
5068 _ => return Err(runtime_err("mssql() query must be a string")),
5069 };
5070 let df = self
5071 .engine()
5072 .read_mssql(&conn_str, &query)
5073 .map_err(runtime_err)?;
5074 Ok(VmValue::Table(VmTable { df }))
5075 }
5076 #[cfg(not(feature = "mssql"))]
5077 Err(runtime_err("mssql() requires the 'mssql' feature"))
5078 }
5079 #[cfg(feature = "native")]
5080 BuiltinId::ReadSnowflake => {
5081 #[cfg(feature = "snowflake")]
5082 {
5083 if args.len() < 2 {
5084 return Err(runtime_err("snowflake() expects (config, query)"));
5085 }
5086 let config = match &args[0] {
5087 VmValue::String(s) => {
5088 let s_str = s.to_string();
5089 resolve_tl_config_connection(&s_str)
5090 }
5091 _ => return Err(runtime_err("snowflake() config must be a string")),
5092 };
5093 let query = match &args[1] {
5094 VmValue::String(s) => s.to_string(),
5095 _ => return Err(runtime_err("snowflake() query must be a string")),
5096 };
5097 let df = self
5098 .engine()
5099 .read_snowflake(&config, &query)
5100 .map_err(runtime_err)?;
5101 Ok(VmValue::Table(VmTable { df }))
5102 }
5103 #[cfg(not(feature = "snowflake"))]
5104 Err(runtime_err("snowflake() requires the 'snowflake' feature"))
5105 }
5106 #[cfg(feature = "native")]
5107 BuiltinId::ReadBigQuery => {
5108 #[cfg(feature = "bigquery")]
5109 {
5110 if args.len() < 2 {
5111 return Err(runtime_err("bigquery() expects (config, query)"));
5112 }
5113 let config = match &args[0] {
5114 VmValue::String(s) => {
5115 let s_str = s.to_string();
5116 resolve_tl_config_connection(&s_str)
5117 }
5118 _ => return Err(runtime_err("bigquery() config must be a string")),
5119 };
5120 let query = match &args[1] {
5121 VmValue::String(s) => s.to_string(),
5122 _ => return Err(runtime_err("bigquery() query must be a string")),
5123 };
5124 let df = self
5125 .engine()
5126 .read_bigquery(&config, &query)
5127 .map_err(runtime_err)?;
5128 Ok(VmValue::Table(VmTable { df }))
5129 }
5130 #[cfg(not(feature = "bigquery"))]
5131 Err(runtime_err("bigquery() requires the 'bigquery' feature"))
5132 }
5133 #[cfg(feature = "native")]
5134 BuiltinId::ReadDatabricks => {
5135 #[cfg(feature = "databricks")]
5136 {
5137 if args.len() < 2 {
5138 return Err(runtime_err("databricks() expects (config, query)"));
5139 }
5140 let config = match &args[0] {
5141 VmValue::String(s) => {
5142 let s_str = s.to_string();
5143 resolve_tl_config_connection(&s_str)
5144 }
5145 _ => return Err(runtime_err("databricks() config must be a string")),
5146 };
5147 let query = match &args[1] {
5148 VmValue::String(s) => s.to_string(),
5149 _ => return Err(runtime_err("databricks() query must be a string")),
5150 };
5151 let df = self
5152 .engine()
5153 .read_databricks(&config, &query)
5154 .map_err(runtime_err)?;
5155 Ok(VmValue::Table(VmTable { df }))
5156 }
5157 #[cfg(not(feature = "databricks"))]
5158 Err(runtime_err(
5159 "databricks() requires the 'databricks' feature",
5160 ))
5161 }
5162 #[cfg(feature = "native")]
5163 BuiltinId::ReadClickHouse => {
5164 #[cfg(feature = "clickhouse")]
5165 {
5166 if args.len() < 2 {
5167 return Err(runtime_err("clickhouse() expects (url, query)"));
5168 }
5169 let url = match &args[0] {
5170 VmValue::String(s) => {
5171 let s_str = s.to_string();
5172 resolve_tl_config_connection(&s_str)
5173 }
5174 _ => return Err(runtime_err("clickhouse() url must be a string")),
5175 };
5176 let query = match &args[1] {
5177 VmValue::String(s) => s.to_string(),
5178 _ => return Err(runtime_err("clickhouse() query must be a string")),
5179 };
5180 let df = self
5181 .engine()
5182 .read_clickhouse(&url, &query)
5183 .map_err(runtime_err)?;
5184 Ok(VmValue::Table(VmTable { df }))
5185 }
5186 #[cfg(not(feature = "clickhouse"))]
5187 Err(runtime_err(
5188 "clickhouse() requires the 'clickhouse' feature",
5189 ))
5190 }
5191 #[cfg(feature = "native")]
5192 BuiltinId::ReadMongo => {
5193 #[cfg(feature = "mongodb")]
5194 {
5195 if args.len() < 4 {
5196 return Err(runtime_err(
5197 "mongo() expects (conn_str, database, collection, filter_json)",
5198 ));
5199 }
5200 let conn_str = match &args[0] {
5201 VmValue::String(s) => {
5202 let s_str = s.to_string();
5203 resolve_tl_config_connection(&s_str)
5204 }
5205 _ => return Err(runtime_err("mongo() conn_str must be a string")),
5206 };
5207 let database = match &args[1] {
5208 VmValue::String(s) => s.to_string(),
5209 _ => return Err(runtime_err("mongo() database must be a string")),
5210 };
5211 let collection = match &args[2] {
5212 VmValue::String(s) => s.to_string(),
5213 _ => return Err(runtime_err("mongo() collection must be a string")),
5214 };
5215 let filter_json = match &args[3] {
5216 VmValue::String(s) => s.to_string(),
5217 _ => return Err(runtime_err("mongo() filter must be a string")),
5218 };
5219 let df = self
5220 .engine()
5221 .read_mongo(&conn_str, &database, &collection, &filter_json)
5222 .map_err(runtime_err)?;
5223 Ok(VmValue::Table(VmTable { df }))
5224 }
5225 #[cfg(not(feature = "mongodb"))]
5226 Err(runtime_err("mongo() requires the 'mongodb' feature"))
5227 }
5228 #[cfg(feature = "native")]
5229 BuiltinId::SftpDownload => {
5230 #[cfg(feature = "sftp")]
5231 {
5232 if args.len() < 3 {
5233 return Err(runtime_err(
5234 "sftp_download() expects (config, remote_path, local_path)",
5235 ));
5236 }
5237 let config = match &args[0] {
5238 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5239 _ => return Err(runtime_err("sftp_download() config must be a string")),
5240 };
5241 let remote = match &args[1] {
5242 VmValue::String(s) => s.to_string(),
5243 _ => {
5244 return Err(runtime_err(
5245 "sftp_download() remote_path must be a string",
5246 ));
5247 }
5248 };
5249 let local = match &args[2] {
5250 VmValue::String(s) => s.to_string(),
5251 _ => {
5252 return Err(runtime_err("sftp_download() local_path must be a string"));
5253 }
5254 };
5255 let result = self
5256 .engine()
5257 .sftp_download(&config, &remote, &local)
5258 .map_err(runtime_err)?;
5259 Ok(VmValue::String(Arc::from(result.as_str())))
5260 }
5261 #[cfg(not(feature = "sftp"))]
5262 Err(runtime_err("sftp_download() requires the 'sftp' feature"))
5263 }
5264 #[cfg(feature = "native")]
5265 BuiltinId::SftpUpload => {
5266 #[cfg(feature = "sftp")]
5267 {
5268 if args.len() < 3 {
5269 return Err(runtime_err(
5270 "sftp_upload() expects (config, local_path, remote_path)",
5271 ));
5272 }
5273 let config = match &args[0] {
5274 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5275 _ => return Err(runtime_err("sftp_upload() config must be a string")),
5276 };
5277 let local = match &args[1] {
5278 VmValue::String(s) => s.to_string(),
5279 _ => return Err(runtime_err("sftp_upload() local_path must be a string")),
5280 };
5281 let remote = match &args[2] {
5282 VmValue::String(s) => s.to_string(),
5283 _ => return Err(runtime_err("sftp_upload() remote_path must be a string")),
5284 };
5285 let result = self
5286 .engine()
5287 .sftp_upload(&config, &local, &remote)
5288 .map_err(runtime_err)?;
5289 Ok(VmValue::String(Arc::from(result.as_str())))
5290 }
5291 #[cfg(not(feature = "sftp"))]
5292 Err(runtime_err("sftp_upload() requires the 'sftp' feature"))
5293 }
5294 #[cfg(feature = "native")]
5295 BuiltinId::SftpList => {
5296 #[cfg(feature = "sftp")]
5297 {
5298 if args.len() < 2 {
5299 return Err(runtime_err("sftp_list() expects (config, remote_path)"));
5300 }
5301 let config = match &args[0] {
5302 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5303 _ => return Err(runtime_err("sftp_list() config must be a string")),
5304 };
5305 let remote = match &args[1] {
5306 VmValue::String(s) => s.to_string(),
5307 _ => return Err(runtime_err("sftp_list() remote_path must be a string")),
5308 };
5309 let df = self
5310 .engine()
5311 .sftp_list(&config, &remote)
5312 .map_err(runtime_err)?;
5313 Ok(VmValue::Table(VmTable { df }))
5314 }
5315 #[cfg(not(feature = "sftp"))]
5316 Err(runtime_err("sftp_list() requires the 'sftp' feature"))
5317 }
5318 #[cfg(feature = "native")]
5319 BuiltinId::SftpReadCsv => {
5320 #[cfg(feature = "sftp")]
5321 {
5322 if args.len() < 2 {
5323 return Err(runtime_err("sftp_read_csv() expects (config, remote_path)"));
5324 }
5325 let config = match &args[0] {
5326 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5327 _ => return Err(runtime_err("sftp_read_csv() config must be a string")),
5328 };
5329 let remote = match &args[1] {
5330 VmValue::String(s) => s.to_string(),
5331 _ => {
5332 return Err(runtime_err(
5333 "sftp_read_csv() remote_path must be a string",
5334 ));
5335 }
5336 };
5337 let df = self
5338 .engine()
5339 .sftp_read_csv(&config, &remote)
5340 .map_err(runtime_err)?;
5341 Ok(VmValue::Table(VmTable { df }))
5342 }
5343 #[cfg(not(feature = "sftp"))]
5344 Err(runtime_err("sftp_read_csv() requires the 'sftp' feature"))
5345 }
5346 #[cfg(feature = "native")]
5347 BuiltinId::SftpReadParquet => {
5348 #[cfg(feature = "sftp")]
5349 {
5350 if args.len() < 2 {
5351 return Err(runtime_err(
5352 "sftp_read_parquet() expects (config, remote_path)",
5353 ));
5354 }
5355 let config = match &args[0] {
5356 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5357 _ => {
5358 return Err(runtime_err("sftp_read_parquet() config must be a string"));
5359 }
5360 };
5361 let remote = match &args[1] {
5362 VmValue::String(s) => s.to_string(),
5363 _ => {
5364 return Err(runtime_err(
5365 "sftp_read_parquet() remote_path must be a string",
5366 ));
5367 }
5368 };
5369 let df = self
5370 .engine()
5371 .sftp_read_parquet(&config, &remote)
5372 .map_err(runtime_err)?;
5373 Ok(VmValue::Table(VmTable { df }))
5374 }
5375 #[cfg(not(feature = "sftp"))]
5376 Err(runtime_err(
5377 "sftp_read_parquet() requires the 'sftp' feature",
5378 ))
5379 }
5380 #[cfg(feature = "native")]
5381 BuiltinId::RedisConnect => {
5382 #[cfg(feature = "redis")]
5383 {
5384 if args.is_empty() {
5385 return Err(runtime_err("redis_connect() expects (url)"));
5386 }
5387 let url = match &args[0] {
5388 VmValue::String(s) => s.to_string(),
5389 _ => return Err(runtime_err("redis_connect() url must be a string")),
5390 };
5391 let result = tl_data::redis_conn::redis_connect(&url).map_err(runtime_err)?;
5392 Ok(VmValue::String(Arc::from(result.as_str())))
5393 }
5394 #[cfg(not(feature = "redis"))]
5395 Err(runtime_err("redis_connect() requires the 'redis' feature"))
5396 }
5397 #[cfg(feature = "native")]
5398 BuiltinId::RedisGet => {
5399 #[cfg(feature = "redis")]
5400 {
5401 if args.len() < 2 {
5402 return Err(runtime_err("redis_get() expects (url, key)"));
5403 }
5404 let url = match &args[0] {
5405 VmValue::String(s) => s.to_string(),
5406 _ => return Err(runtime_err("redis_get() url must be a string")),
5407 };
5408 let key = match &args[1] {
5409 VmValue::String(s) => s.to_string(),
5410 _ => return Err(runtime_err("redis_get() key must be a string")),
5411 };
5412 match tl_data::redis_conn::redis_get(&url, &key).map_err(runtime_err)? {
5413 Some(v) => Ok(VmValue::String(Arc::from(v.as_str()))),
5414 None => Ok(VmValue::None),
5415 }
5416 }
5417 #[cfg(not(feature = "redis"))]
5418 Err(runtime_err("redis_get() requires the 'redis' feature"))
5419 }
5420 #[cfg(feature = "native")]
5421 BuiltinId::RedisSet => {
5422 #[cfg(feature = "redis")]
5423 {
5424 if args.len() < 3 {
5425 return Err(runtime_err("redis_set() expects (url, key, value)"));
5426 }
5427 let url = match &args[0] {
5428 VmValue::String(s) => s.to_string(),
5429 _ => return Err(runtime_err("redis_set() url must be a string")),
5430 };
5431 let key = match &args[1] {
5432 VmValue::String(s) => s.to_string(),
5433 _ => return Err(runtime_err("redis_set() key must be a string")),
5434 };
5435 let value = match &args[2] {
5436 VmValue::String(s) => s.to_string(),
5437 _ => format!("{}", &args[2]),
5438 };
5439 tl_data::redis_conn::redis_set(&url, &key, &value).map_err(runtime_err)?;
5440 Ok(VmValue::None)
5441 }
5442 #[cfg(not(feature = "redis"))]
5443 Err(runtime_err("redis_set() requires the 'redis' feature"))
5444 }
5445 #[cfg(feature = "native")]
5446 BuiltinId::RedisDel => {
5447 #[cfg(feature = "redis")]
5448 {
5449 if args.len() < 2 {
5450 return Err(runtime_err("redis_del() expects (url, key)"));
5451 }
5452 let url = match &args[0] {
5453 VmValue::String(s) => s.to_string(),
5454 _ => return Err(runtime_err("redis_del() url must be a string")),
5455 };
5456 let key = match &args[1] {
5457 VmValue::String(s) => s.to_string(),
5458 _ => return Err(runtime_err("redis_del() key must be a string")),
5459 };
5460 let deleted =
5461 tl_data::redis_conn::redis_del(&url, &key).map_err(runtime_err)?;
5462 Ok(VmValue::Bool(deleted))
5463 }
5464 #[cfg(not(feature = "redis"))]
5465 Err(runtime_err("redis_del() requires the 'redis' feature"))
5466 }
5467 #[cfg(feature = "native")]
5468 BuiltinId::GraphqlQuery => {
5469 if args.len() < 2 {
5470 return Err(runtime_err(
5471 "graphql_query() expects (endpoint, query, [variables])",
5472 ));
5473 }
5474 let endpoint = match &args[0] {
5475 VmValue::String(s) => s.to_string(),
5476 _ => return Err(runtime_err("graphql_query() endpoint must be a string")),
5477 };
5478 let query = match &args[1] {
5479 VmValue::String(s) => s.to_string(),
5480 _ => return Err(runtime_err("graphql_query() query must be a string")),
5481 };
5482 let variables = if args.len() > 2 {
5483 vm_value_to_json(&args[2])
5484 } else {
5485 serde_json::Value::Null
5486 };
5487 let mut body = serde_json::Map::new();
5488 body.insert("query".to_string(), serde_json::Value::String(query));
5489 if !variables.is_null() {
5490 body.insert("variables".to_string(), variables);
5491 }
5492 let client = reqwest::blocking::Client::new();
5493 let resp = client
5494 .post(&endpoint)
5495 .header("Content-Type", "application/json")
5496 .json(&body)
5497 .send()
5498 .map_err(|e| runtime_err(format!("graphql_query() request error: {e}")))?;
5499 let text = resp
5500 .text()
5501 .map_err(|e| runtime_err(format!("graphql_query() response error: {e}")))?;
5502 let json: serde_json::Value = serde_json::from_str(&text)
5503 .map_err(|e| runtime_err(format!("graphql_query() JSON parse error: {e}")))?;
5504 Ok(vm_json_to_value(&json))
5505 }
5506 #[cfg(feature = "native")]
5507 BuiltinId::RegisterS3 => {
5508 #[cfg(feature = "s3")]
5509 {
5510 if args.len() < 2 {
5511 return Err(runtime_err(
5512 "register_s3() expects (bucket, region, [access_key], [secret_key], [endpoint])",
5513 ));
5514 }
5515 let bucket = match &args[0] {
5516 VmValue::String(s) => s.to_string(),
5517 _ => return Err(runtime_err("register_s3() bucket must be a string")),
5518 };
5519 let region = match &args[1] {
5520 VmValue::String(s) => s.to_string(),
5521 _ => return Err(runtime_err("register_s3() region must be a string")),
5522 };
5523 let access_key = args.get(2).and_then(|v| {
5524 if let VmValue::String(s) = v {
5525 Some(s.to_string())
5526 } else {
5527 None
5528 }
5529 });
5530 let secret_key = args.get(3).and_then(|v| {
5531 if let VmValue::String(s) = v {
5532 Some(s.to_string())
5533 } else {
5534 None
5535 }
5536 });
5537 let endpoint = args.get(4).and_then(|v| {
5538 if let VmValue::String(s) = v {
5539 Some(s.to_string())
5540 } else {
5541 None
5542 }
5543 });
5544 self.engine()
5545 .register_s3(
5546 &bucket,
5547 ®ion,
5548 access_key.as_deref(),
5549 secret_key.as_deref(),
5550 endpoint.as_deref(),
5551 )
5552 .map_err(runtime_err)?;
5553 Ok(VmValue::None)
5554 }
5555 #[cfg(not(feature = "s3"))]
5556 Err(runtime_err("register_s3() requires the 's3' feature"))
5557 }
5558 #[cfg(not(feature = "native"))]
5559 BuiltinId::ReadMysql
5560 | BuiltinId::ReadSqlite
5561 | BuiltinId::WriteSqlite
5562 | BuiltinId::ReadDuckDb
5563 | BuiltinId::WriteDuckDb
5564 | BuiltinId::ReadRedshift
5565 | BuiltinId::ReadMssql
5566 | BuiltinId::ReadSnowflake
5567 | BuiltinId::ReadBigQuery
5568 | BuiltinId::ReadDatabricks
5569 | BuiltinId::ReadClickHouse
5570 | BuiltinId::ReadMongo
5571 | BuiltinId::SftpDownload
5572 | BuiltinId::SftpUpload
5573 | BuiltinId::SftpList
5574 | BuiltinId::SftpReadCsv
5575 | BuiltinId::SftpReadParquet
5576 | BuiltinId::RedisConnect
5577 | BuiltinId::RedisGet
5578 | BuiltinId::RedisSet
5579 | BuiltinId::RedisDel
5580 | BuiltinId::GraphqlQuery
5581 | BuiltinId::RegisterS3 => Err(runtime_err("Connectors not available in WASM")),
5582 BuiltinId::PyImport => {
5584 self.check_permission("python")?;
5585 #[cfg(feature = "python")]
5586 {
5587 crate::python::py_import_impl(&args)
5588 }
5589 #[cfg(not(feature = "python"))]
5590 Err(runtime_err("py_import() requires the 'python' feature"))
5591 }
5592 BuiltinId::PyCall => {
5593 self.check_permission("python")?;
5594 #[cfg(feature = "python")]
5595 {
5596 crate::python::py_call_impl(&args)
5597 }
5598 #[cfg(not(feature = "python"))]
5599 Err(runtime_err("py_call() requires the 'python' feature"))
5600 }
5601 BuiltinId::PyEval => {
5602 self.check_permission("python")?;
5603 #[cfg(feature = "python")]
5604 {
5605 crate::python::py_eval_impl(&args)
5606 }
5607 #[cfg(not(feature = "python"))]
5608 Err(runtime_err("py_eval() requires the 'python' feature"))
5609 }
5610 BuiltinId::PyGetAttr => {
5611 self.check_permission("python")?;
5612 #[cfg(feature = "python")]
5613 {
5614 crate::python::py_getattr_impl(&args)
5615 }
5616 #[cfg(not(feature = "python"))]
5617 Err(runtime_err("py_getattr() requires the 'python' feature"))
5618 }
5619 BuiltinId::PySetAttr => {
5620 self.check_permission("python")?;
5621 #[cfg(feature = "python")]
5622 {
5623 crate::python::py_setattr_impl(&args)
5624 }
5625 #[cfg(not(feature = "python"))]
5626 Err(runtime_err("py_setattr() requires the 'python' feature"))
5627 }
5628 BuiltinId::PyToTl => {
5629 #[cfg(feature = "python")]
5630 {
5631 crate::python::py_to_tl_impl(&args)
5632 }
5633 #[cfg(not(feature = "python"))]
5634 Err(runtime_err("py_to_tl() requires the 'python' feature"))
5635 }
5636
5637 #[cfg(feature = "native")]
5639 BuiltinId::SchemaRegister => {
5640 let name = match args.first() {
5641 Some(VmValue::String(s)) => s.to_string(),
5642 _ => {
5643 return Err(runtime_err(
5644 "schema_register: first arg must be schema name string",
5645 ));
5646 }
5647 };
5648 let version = match args.get(1) {
5649 Some(VmValue::Int(v)) => *v,
5650 _ => {
5651 return Err(runtime_err(
5652 "schema_register: second arg must be version number",
5653 ));
5654 }
5655 };
5656 let fields = match args.get(2) {
5657 Some(VmValue::Map(pairs)) => {
5658 let mut arrow_fields = Vec::new();
5659 for (k, v) in pairs.iter() {
5660 let fname = k.to_string();
5661 let ftype = match v {
5662 VmValue::String(s) => s.to_string(),
5663 _ => "string".to_string(),
5664 };
5665 arrow_fields.push(tl_data::ArrowField::new(
5666 &fname,
5667 crate::schema::type_name_to_arrow_pub(&ftype),
5668 true,
5669 ));
5670 }
5671 arrow_fields
5672 }
5673 _ => return Err(runtime_err("schema_register: third arg must be fields map")),
5674 };
5675 let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(fields));
5676 self.schema_registry
5677 .register(
5678 &name,
5679 version,
5680 schema,
5681 crate::schema::SchemaMetadata::default(),
5682 )
5683 .map_err(|e| runtime_err(&e))?;
5684 Ok(VmValue::None)
5685 }
5686 #[cfg(feature = "native")]
5687 BuiltinId::SchemaGet => {
5688 let name = match args.first() {
5689 Some(VmValue::String(s)) => s.to_string(),
5690 _ => return Err(runtime_err("schema_get: need name")),
5691 };
5692 let version = match args.get(1) {
5693 Some(VmValue::Int(v)) => *v,
5694 _ => return Err(runtime_err("schema_get: need version")),
5695 };
5696 match self.schema_registry.get(&name, version) {
5697 Some(vs) => {
5698 let fields: Vec<VmValue> = vs
5699 .schema
5700 .fields()
5701 .iter()
5702 .map(|f| {
5703 VmValue::String(std::sync::Arc::from(format!(
5704 "{}: {}",
5705 f.name(),
5706 f.data_type()
5707 )))
5708 })
5709 .collect();
5710 Ok(VmValue::List(Box::new(fields)))
5711 }
5712 None => Ok(VmValue::None),
5713 }
5714 }
5715 #[cfg(feature = "native")]
5716 BuiltinId::SchemaLatest => {
5717 let name = match args.first() {
5718 Some(VmValue::String(s)) => s.to_string(),
5719 _ => return Err(runtime_err("schema_latest: need name")),
5720 };
5721 match self.schema_registry.latest(&name) {
5722 Some(vs) => Ok(VmValue::Int(vs.version)),
5723 None => Ok(VmValue::None),
5724 }
5725 }
5726 #[cfg(feature = "native")]
5727 BuiltinId::SchemaHistory => {
5728 let name = match args.first() {
5729 Some(VmValue::String(s)) => s.to_string(),
5730 _ => return Err(runtime_err("schema_history: need name")),
5731 };
5732 let versions = self.schema_registry.versions(&name);
5733 Ok(VmValue::List(Box::new(
5734 versions.into_iter().map(VmValue::Int).collect(),
5735 )))
5736 }
5737 #[cfg(feature = "native")]
5738 BuiltinId::SchemaCheck => {
5739 let name = match args.first() {
5740 Some(VmValue::String(s)) => s.to_string(),
5741 _ => return Err(runtime_err("schema_check: need name")),
5742 };
5743 let v1 = match args.get(1) {
5744 Some(VmValue::Int(v)) => *v,
5745 _ => return Err(runtime_err("schema_check: need v1")),
5746 };
5747 let v2 = match args.get(2) {
5748 Some(VmValue::Int(v)) => *v,
5749 _ => return Err(runtime_err("schema_check: need v2")),
5750 };
5751 let mode_str = match args.get(3) {
5752 Some(VmValue::String(s)) => s.to_string(),
5753 _ => "backward".to_string(),
5754 };
5755 let mode = crate::schema::CompatibilityMode::from_str(&mode_str);
5756 let issues = self
5757 .schema_registry
5758 .check_compatibility(&name, v1, v2, mode);
5759 Ok(VmValue::List(Box::new(
5760 issues
5761 .into_iter()
5762 .map(|i| VmValue::String(std::sync::Arc::from(i.to_string())))
5763 .collect(),
5764 )))
5765 }
5766 #[cfg(feature = "native")]
5767 BuiltinId::SchemaDiff => {
5768 let name = match args.first() {
5769 Some(VmValue::String(s)) => s.to_string(),
5770 _ => return Err(runtime_err("schema_diff: need name")),
5771 };
5772 let v1 = match args.get(1) {
5773 Some(VmValue::Int(v)) => *v,
5774 _ => return Err(runtime_err("schema_diff: need v1")),
5775 };
5776 let v2 = match args.get(2) {
5777 Some(VmValue::Int(v)) => *v,
5778 _ => return Err(runtime_err("schema_diff: need v2")),
5779 };
5780 let diffs = self.schema_registry.diff(&name, v1, v2);
5781 Ok(VmValue::List(Box::new(
5782 diffs
5783 .into_iter()
5784 .map(|d| VmValue::String(std::sync::Arc::from(d.to_string())))
5785 .collect(),
5786 )))
5787 }
5788 #[cfg(feature = "native")]
5789 BuiltinId::SchemaApplyMigration => {
5790 let name = match args.first() {
5791 Some(VmValue::String(s)) => s.to_string(),
5792 _ => return Err(runtime_err("schema_apply_migration: need name")),
5793 };
5794 let from_v = match args.get(1) {
5795 Some(VmValue::Int(v)) => *v,
5796 _ => return Err(runtime_err("schema_apply_migration: need from_ver")),
5797 };
5798 let to_v = match args.get(2) {
5799 Some(VmValue::Int(v)) => *v,
5800 _ => return Err(runtime_err("schema_apply_migration: need to_ver")),
5801 };
5802 Ok(VmValue::String(std::sync::Arc::from(format!(
5803 "migration {}:{}->{} applied",
5804 name, from_v, to_v
5805 ))))
5806 }
5807 #[cfg(feature = "native")]
5808 BuiltinId::SchemaVersions => {
5809 let name = match args.first() {
5810 Some(VmValue::String(s)) => s.to_string(),
5811 _ => return Err(runtime_err("schema_versions: need name")),
5812 };
5813 let versions = self.schema_registry.versions(&name);
5814 Ok(VmValue::List(Box::new(
5815 versions.into_iter().map(VmValue::Int).collect(),
5816 )))
5817 }
5818 #[cfg(feature = "native")]
5819 BuiltinId::SchemaFields => {
5820 let name = match args.first() {
5821 Some(VmValue::String(s)) => s.to_string(),
5822 _ => return Err(runtime_err("schema_fields: need name")),
5823 };
5824 let version = match args.get(1) {
5825 Some(VmValue::Int(v)) => *v,
5826 _ => return Err(runtime_err("schema_fields: need version")),
5827 };
5828 let fields = self.schema_registry.fields(&name, version);
5829 Ok(VmValue::List(Box::new(
5830 fields
5831 .into_iter()
5832 .map(|(n, t)| {
5833 VmValue::String(std::sync::Arc::from(format!("{}: {}", n, t)))
5834 })
5835 .collect(),
5836 )))
5837 }
5838 #[cfg(not(feature = "native"))]
5839 BuiltinId::SchemaRegister
5840 | BuiltinId::SchemaGet
5841 | BuiltinId::SchemaLatest
5842 | BuiltinId::SchemaHistory
5843 | BuiltinId::SchemaCheck
5844 | BuiltinId::SchemaDiff
5845 | BuiltinId::SchemaApplyMigration
5846 | BuiltinId::SchemaVersions
5847 | BuiltinId::SchemaFields => {
5848 let _ = args;
5849 Err(runtime_err("Schema operations not available in WASM"))
5850 }
5851
5852 BuiltinId::Decimal => {
5854 use std::str::FromStr;
5855 let s = match args.first() {
5856 Some(VmValue::String(s)) => s.to_string(),
5857 Some(VmValue::Int(n)) => n.to_string(),
5858 Some(VmValue::Float(f)) => f.to_string(),
5859 _ => return Err(runtime_err("decimal(): expected string, int, or float")),
5860 };
5861 let d = rust_decimal::Decimal::from_str(&s)
5862 .map_err(|e| runtime_err(format!("decimal(): invalid: {e}")))?;
5863 Ok(VmValue::Decimal(d))
5864 }
5865
5866 BuiltinId::SecretGet => {
5868 let key = match args.first() {
5869 Some(VmValue::String(s)) => s.to_string(),
5870 _ => return Err(runtime_err("secret_get: need key")),
5871 };
5872 if let Some(val) = self.secret_vault.get(&key) {
5873 Ok(VmValue::Secret(Arc::from(val.as_str())))
5874 } else {
5875 let env_key = format!("TL_SECRET_{}", key.to_uppercase());
5877 match std::env::var(&env_key) {
5878 Ok(val) => Ok(VmValue::Secret(Arc::from(val.as_str()))),
5879 Err(_) => Ok(VmValue::None),
5880 }
5881 }
5882 }
5883 BuiltinId::SecretSet => {
5884 let key = match args.first() {
5885 Some(VmValue::String(s)) => s.to_string(),
5886 _ => return Err(runtime_err("secret_set: need key")),
5887 };
5888 let val = match args.get(1) {
5889 Some(VmValue::String(s)) => s.to_string(),
5890 Some(VmValue::Secret(s)) => s.to_string(),
5891 _ => return Err(runtime_err("secret_set: need value")),
5892 };
5893 self.secret_vault.insert(key, val);
5894 Ok(VmValue::None)
5895 }
5896 BuiltinId::SecretDelete => {
5897 let key = match args.first() {
5898 Some(VmValue::String(s)) => s.to_string(),
5899 _ => return Err(runtime_err("secret_delete: need key")),
5900 };
5901 self.secret_vault.remove(&key);
5902 Ok(VmValue::None)
5903 }
5904 BuiltinId::SecretList => {
5905 let keys: Vec<VmValue> = self
5906 .secret_vault
5907 .keys()
5908 .map(|k| VmValue::String(Arc::from(k.as_str())))
5909 .collect();
5910 Ok(VmValue::List(Box::new(keys)))
5911 }
5912 BuiltinId::CheckPermission => {
5913 let perm = match args.first() {
5914 Some(VmValue::String(s)) => s.to_string(),
5915 _ => return Err(runtime_err("check_permission: need permission name")),
5916 };
5917 let allowed = match self.security_policy {
5918 Some(ref policy) => policy.check(&perm),
5919 None => true,
5920 };
5921 Ok(VmValue::Bool(allowed))
5922 }
5923 BuiltinId::MaskEmail => {
5924 let email = match args.first() {
5925 Some(VmValue::String(s)) => s.to_string(),
5926 _ => return Err(runtime_err("mask_email: need string")),
5927 };
5928 let masked = if let Some(at_pos) = email.find('@') {
5929 let local = &email[..at_pos];
5930 let domain = &email[at_pos..];
5931 if local.len() > 1 {
5932 format!("{}***{}", &local[..1], domain)
5933 } else {
5934 format!("***{domain}")
5935 }
5936 } else {
5937 "***".to_string()
5938 };
5939 Ok(VmValue::String(Arc::from(masked.as_str())))
5940 }
5941 BuiltinId::MaskPhone => {
5942 let phone = match args.first() {
5943 Some(VmValue::String(s)) => s.to_string(),
5944 _ => return Err(runtime_err("mask_phone: need string")),
5945 };
5946 let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
5947 let masked = if digits.len() >= 4 {
5948 let last4 = &digits[digits.len() - 4..];
5949 format!("***-***-{last4}")
5950 } else {
5951 "***".to_string()
5952 };
5953 Ok(VmValue::String(Arc::from(masked.as_str())))
5954 }
5955 BuiltinId::MaskCreditCard => {
5956 let cc = match args.first() {
5957 Some(VmValue::String(s)) => s.to_string(),
5958 _ => return Err(runtime_err("mask_cc: need string")),
5959 };
5960 let digits: String = cc.chars().filter(|c| c.is_ascii_digit()).collect();
5961 let masked = if digits.len() >= 4 {
5962 let last4 = &digits[digits.len() - 4..];
5963 format!("****-****-****-{last4}")
5964 } else {
5965 "****-****-****-****".to_string()
5966 };
5967 Ok(VmValue::String(Arc::from(masked.as_str())))
5968 }
5969 BuiltinId::Redact => {
5970 let val = match args.first() {
5971 Some(v) => format!("{v}"),
5972 _ => return Err(runtime_err("redact: need value")),
5973 };
5974 let policy = match args.get(1) {
5975 Some(VmValue::String(s)) => s.to_string(),
5976 _ => "full".to_string(),
5977 };
5978 let result = match policy.as_str() {
5979 "full" => "***".to_string(),
5980 "partial" => {
5981 if val.len() > 2 {
5982 format!("{}***{}", &val[..1], &val[val.len() - 1..])
5983 } else {
5984 "***".to_string()
5985 }
5986 }
5987 "hash" => {
5988 use sha2::Digest;
5989 let hash = sha2::Sha256::digest(val.as_bytes());
5990 format!("{:x}", hash)
5991 }
5992 _ => "***".to_string(),
5993 };
5994 Ok(VmValue::String(Arc::from(result.as_str())))
5995 }
5996 BuiltinId::Hash => {
5997 let val = match args.first() {
5998 Some(VmValue::String(s)) => s.to_string(),
5999 _ => return Err(runtime_err("hash: need string")),
6000 };
6001 let algo = match args.get(1) {
6002 Some(VmValue::String(s)) => s.to_string(),
6003 _ => "sha256".to_string(),
6004 };
6005 let result = match algo.as_str() {
6006 "sha256" => {
6007 use sha2::Digest;
6008 format!("{:x}", sha2::Sha256::digest(val.as_bytes()))
6009 }
6010 "sha512" => {
6011 use sha2::Digest;
6012 format!("{:x}", sha2::Sha512::digest(val.as_bytes()))
6013 }
6014 "md5" => {
6015 use md5::Digest;
6016 format!("{:x}", md5::Md5::digest(val.as_bytes()))
6017 }
6018 _ => {
6019 return Err(runtime_err(format!(
6020 "hash: unknown algorithm '{algo}' (use sha256, sha512, or md5)"
6021 )));
6022 }
6023 };
6024 Ok(VmValue::String(Arc::from(result.as_str())))
6025 }
6026
6027 #[cfg(feature = "async-runtime")]
6029 BuiltinId::AsyncReadFile => {
6030 let rt = self.ensure_runtime();
6031 crate::async_runtime::async_read_file_impl(&rt, &args, &self.security_policy)
6032 }
6033 #[cfg(feature = "async-runtime")]
6034 BuiltinId::AsyncWriteFile => {
6035 let rt = self.ensure_runtime();
6036 crate::async_runtime::async_write_file_impl(&rt, &args, &self.security_policy)
6037 }
6038 #[cfg(feature = "async-runtime")]
6039 BuiltinId::AsyncHttpGet => {
6040 let rt = self.ensure_runtime();
6041 crate::async_runtime::async_http_get_impl(&rt, &args, &self.security_policy)
6042 }
6043 #[cfg(feature = "async-runtime")]
6044 BuiltinId::AsyncHttpPost => {
6045 let rt = self.ensure_runtime();
6046 crate::async_runtime::async_http_post_impl(&rt, &args, &self.security_policy)
6047 }
6048 #[cfg(feature = "async-runtime")]
6049 BuiltinId::AsyncSleep => {
6050 let rt = self.ensure_runtime();
6051 crate::async_runtime::async_sleep_impl(&rt, &args)
6052 }
6053 #[cfg(feature = "async-runtime")]
6054 BuiltinId::Select => crate::async_runtime::select_impl(&args),
6055 #[cfg(feature = "async-runtime")]
6056 BuiltinId::RaceAll => crate::async_runtime::race_all_impl(&args),
6057 #[cfg(feature = "async-runtime")]
6058 BuiltinId::AsyncMap => {
6059 let rt = self.ensure_runtime();
6060 let stack_snapshot = self.stack.clone();
6061 crate::async_runtime::async_map_impl(&rt, &args, &self.globals, &stack_snapshot)
6062 }
6063 #[cfg(feature = "async-runtime")]
6064 BuiltinId::AsyncFilter => {
6065 let rt = self.ensure_runtime();
6066 let stack_snapshot = self.stack.clone();
6067 crate::async_runtime::async_filter_impl(&rt, &args, &self.globals, &stack_snapshot)
6068 }
6069
6070 #[cfg(not(feature = "async-runtime"))]
6071 BuiltinId::AsyncReadFile
6072 | BuiltinId::AsyncWriteFile
6073 | BuiltinId::AsyncHttpGet
6074 | BuiltinId::AsyncHttpPost
6075 | BuiltinId::AsyncSleep
6076 | BuiltinId::Select
6077 | BuiltinId::AsyncMap
6078 | BuiltinId::AsyncFilter
6079 | BuiltinId::RaceAll => Err(runtime_err(format!(
6080 "{}: async builtins require the 'async-runtime' feature",
6081 builtin_id.name()
6082 ))),
6083
6084 BuiltinId::IsError => {
6086 if args.is_empty() {
6087 return Err(runtime_err("is_error() expects 1 argument"));
6088 }
6089 let is_err = matches!(&args[0], VmValue::EnumInstance(e) if
6090 &*e.type_name == "DataError" ||
6091 &*e.type_name == "NetworkError" ||
6092 &*e.type_name == "ConnectorError"
6093 );
6094 Ok(VmValue::Bool(is_err))
6095 }
6096 BuiltinId::ErrorType => {
6097 if args.is_empty() {
6098 return Err(runtime_err("error_type() expects 1 argument"));
6099 }
6100 match &args[0] {
6101 VmValue::EnumInstance(e) => Ok(VmValue::String(e.type_name.clone())),
6102 _ => Ok(VmValue::None),
6103 }
6104 }
6105
6106 #[cfg(feature = "gpu")]
6108 BuiltinId::GpuAvailable => Ok(VmValue::Bool(tl_gpu::GpuDevice::is_available())),
6109 #[cfg(not(feature = "gpu"))]
6110 BuiltinId::GpuAvailable => Ok(VmValue::Bool(false)),
6111
6112 #[cfg(feature = "gpu")]
6113 BuiltinId::ToGpu => {
6114 if args.is_empty() {
6115 return Err(runtime_err("to_gpu() expects 1 argument (tensor)"));
6116 }
6117 let gt = self.ensure_gpu_tensor(&args[0])?;
6118 Ok(VmValue::GpuTensor(gt))
6119 }
6120 #[cfg(not(feature = "gpu"))]
6121 BuiltinId::ToGpu => Err(runtime_err(
6122 "GPU operations not available. Build with --features gpu",
6123 )),
6124
6125 #[cfg(feature = "gpu")]
6126 BuiltinId::ToCpu => {
6127 if args.is_empty() {
6128 return Err(runtime_err("to_cpu() expects 1 argument (gpu_tensor)"));
6129 }
6130 match &args[0] {
6131 VmValue::GpuTensor(gt) => {
6132 let cpu = gt.to_cpu().map_err(runtime_err)?;
6133 Ok(VmValue::Tensor(Arc::new(cpu)))
6134 }
6135 _ => Err(runtime_err(format!(
6136 "to_cpu() expects a gpu_tensor, got {}",
6137 args[0].type_name()
6138 ))),
6139 }
6140 }
6141 #[cfg(not(feature = "gpu"))]
6142 BuiltinId::ToCpu => Err(runtime_err(
6143 "GPU operations not available. Build with --features gpu",
6144 )),
6145
6146 #[cfg(feature = "gpu")]
6147 BuiltinId::GpuMatmul => {
6148 if args.len() < 2 {
6149 return Err(runtime_err("gpu_matmul() expects 2 arguments"));
6150 }
6151 let a = self.ensure_gpu_tensor(&args[0])?;
6152 let b = self.ensure_gpu_tensor(&args[1])?;
6153 let ops = self.get_gpu_ops()?;
6154 let result = ops.matmul(&a, &b).map_err(runtime_err)?;
6155 Ok(VmValue::GpuTensor(Arc::new(result)))
6156 }
6157 #[cfg(not(feature = "gpu"))]
6158 BuiltinId::GpuMatmul => Err(runtime_err(
6159 "GPU operations not available. Build with --features gpu",
6160 )),
6161
6162 #[cfg(feature = "gpu")]
6163 BuiltinId::GpuBatchPredict => {
6164 if args.len() < 2 {
6165 return Err(runtime_err("gpu_batch_predict() expects 2-3 arguments"));
6166 }
6167 match (&args[0], &args[1]) {
6168 (VmValue::Model(model), VmValue::Tensor(input)) => {
6169 let batch_size = args.get(2).and_then(|v| match v {
6170 VmValue::Int(n) => Some(*n as usize),
6171 _ => None,
6172 });
6173 let result =
6174 tl_gpu::BatchInference::batch_predict(model, input, batch_size)
6175 .map_err(runtime_err)?;
6176 Ok(VmValue::Tensor(Arc::new(result)))
6177 }
6178 _ => Err(runtime_err(
6179 "gpu_batch_predict() expects (model, tensor, [batch_size])",
6180 )),
6181 }
6182 }
6183 #[cfg(not(feature = "gpu"))]
6184 BuiltinId::GpuBatchPredict => Err(runtime_err(
6185 "GPU operations not available. Build with --features gpu",
6186 )),
6187 #[cfg(feature = "native")]
6189 BuiltinId::Embed => {
6190 if args.is_empty() {
6191 return Err(runtime_err("embed() requires a text argument"));
6192 }
6193 let text = match &args[0] {
6194 VmValue::String(s) => s.to_string(),
6195 _ => return Err(runtime_err("embed() expects a string")),
6196 };
6197 let model = args
6198 .get(1)
6199 .and_then(|v| match v {
6200 VmValue::String(s) => Some(s.to_string()),
6201 _ => None,
6202 })
6203 .unwrap_or_else(|| "text-embedding-3-small".to_string());
6204 let api_key = args
6205 .get(2)
6206 .and_then(|v| match v {
6207 VmValue::String(s) => Some(s.to_string()),
6208 _ => None,
6209 })
6210 .or_else(|| std::env::var("TL_OPENAI_KEY").ok())
6211 .ok_or_else(|| {
6212 runtime_err(
6213 "embed() requires an API key. Set TL_OPENAI_KEY or pass as 3rd arg",
6214 )
6215 })?;
6216 let tensor = tl_ai::embed::embed_api(&text, "openai", &model, &api_key)
6217 .map_err(|e| runtime_err(format!("embed error: {e}")))?;
6218 Ok(VmValue::Tensor(Arc::new(tensor)))
6219 }
6220 #[cfg(not(feature = "native"))]
6221 BuiltinId::Embed => Err(runtime_err("embed() not available in WASM")),
6222 #[cfg(feature = "native")]
6223 BuiltinId::HttpRequest => {
6224 self.check_permission("network")?;
6225 if args.len() < 2 {
6226 return Err(runtime_err(
6227 "http_request(method, url, headers?, body?) expects at least 2 args",
6228 ));
6229 }
6230 let method = match &args[0] {
6231 VmValue::String(s) => s.to_string(),
6232 _ => return Err(runtime_err("http_request() method must be a string")),
6233 };
6234 let url = match &args[1] {
6235 VmValue::String(s) => s.to_string(),
6236 _ => return Err(runtime_err("http_request() url must be a string")),
6237 };
6238 let client = reqwest::blocking::Client::new();
6239 let mut builder = match method.to_uppercase().as_str() {
6240 "GET" => client.get(&url),
6241 "POST" => client.post(&url),
6242 "PUT" => client.put(&url),
6243 "DELETE" => client.delete(&url),
6244 "PATCH" => client.patch(&url),
6245 "HEAD" => client.head(&url),
6246 _ => return Err(runtime_err(format!("Unsupported HTTP method: {method}"))),
6247 };
6248 if let Some(VmValue::Map(headers)) = args.get(2) {
6250 for (key, val) in headers.iter() {
6251 if let VmValue::String(v) = val {
6252 builder = builder.header(key.as_ref(), v.as_ref());
6253 }
6254 }
6255 }
6256 if let Some(VmValue::String(body)) = args.get(3) {
6258 builder = builder.body(body.as_ref().to_string());
6259 }
6260 let resp = builder
6261 .send()
6262 .map_err(|e| runtime_err(format!("HTTP error: {e}")))?;
6263 let status = resp.status().as_u16() as i64;
6264 let body = resp
6265 .text()
6266 .map_err(|e| runtime_err(format!("HTTP response error: {e}")))?;
6267 Ok(VmValue::Map(Box::new(vec![
6268 (Arc::from("status"), VmValue::Int(status)),
6269 (Arc::from("body"), VmValue::String(Arc::from(body.as_str()))),
6270 ])))
6271 }
6272 #[cfg(not(feature = "native"))]
6273 BuiltinId::HttpRequest => Err(runtime_err("http_request() not available in WASM")),
6274 #[cfg(feature = "native")]
6275 BuiltinId::RunAgent => {
6276 self.check_permission("network")?;
6277 if args.len() < 2 {
6278 return Err(runtime_err(
6279 "run_agent(agent, message, [history]) expects at least 2 arguments",
6280 ));
6281 }
6282 let agent_def = match &args[0] {
6283 VmValue::AgentDef(def) => def.clone(),
6284 _ => return Err(runtime_err("run_agent() first arg must be an agent")),
6285 };
6286 let message = match &args[1] {
6287 VmValue::String(s) => s.to_string(),
6288 _ => return Err(runtime_err("run_agent() second arg must be a string")),
6289 };
6290 let history = if args.len() >= 3 {
6292 match &args[2] {
6293 VmValue::List(items) => {
6294 let mut hist = Vec::new();
6295 for item in items.iter() {
6296 if let VmValue::List(pair) = item
6297 && pair.len() >= 2
6298 {
6299 let role = match &pair[0] {
6300 VmValue::String(s) => s.to_string(),
6301 _ => continue,
6302 };
6303 let content = match &pair[1] {
6304 VmValue::String(s) => s.to_string(),
6305 _ => continue,
6306 };
6307 hist.push((role, content));
6308 }
6309 }
6310 Some(hist)
6311 }
6312 _ => None,
6313 }
6314 } else {
6315 None
6316 };
6317 self.exec_agent_loop(&agent_def, &message, history.as_deref())
6318 }
6319 #[cfg(not(feature = "native"))]
6320 BuiltinId::RunAgent => Err(runtime_err("run_agent() not available in WASM")),
6321
6322 #[cfg(feature = "native")]
6324 BuiltinId::StreamAgent => {
6325 self.check_permission("network")?;
6326 if args.len() < 3 {
6327 return Err(runtime_err(
6328 "stream_agent(agent, message, callback) expects 3 arguments",
6329 ));
6330 }
6331 let agent_def = match &args[0] {
6332 VmValue::AgentDef(def) => def.clone(),
6333 _ => return Err(runtime_err("stream_agent() first arg must be an agent")),
6334 };
6335 let message = match &args[1] {
6336 VmValue::String(s) => s.to_string(),
6337 _ => return Err(runtime_err("stream_agent() second arg must be a string")),
6338 };
6339 let callback = args[2].clone();
6340
6341 let model = &agent_def.model;
6342 let system = agent_def.system_prompt.as_deref();
6343 let base_url = agent_def.base_url.as_deref();
6344 let api_key = agent_def.api_key.as_deref();
6345
6346 let messages = vec![serde_json::json!({"role": "user", "content": &message})];
6347 let mut reader = tl_ai::stream_chat(model, system, &messages, base_url, api_key)
6348 .map_err(|e| runtime_err(format!("Stream error: {e}")))?;
6349
6350 let mut full_text = String::new();
6351 loop {
6352 match reader.next_chunk() {
6353 Ok(Some(chunk)) => {
6354 full_text.push_str(&chunk);
6355 let chunk_val = VmValue::String(Arc::from(&*chunk));
6356 let _ = self.call_value(callback.clone(), &[chunk_val]);
6357 }
6358 Ok(None) => break,
6359 Err(e) => return Err(runtime_err(format!("Stream error: {e}"))),
6360 }
6361 }
6362
6363 Ok(VmValue::String(Arc::from(&*full_text)))
6364 }
6365 #[cfg(not(feature = "native"))]
6366 BuiltinId::StreamAgent => Err(runtime_err("stream_agent() not available in WASM")),
6367
6368 #[cfg(feature = "native")]
6370 BuiltinId::Random => {
6371 let mut rng = rand::thread_rng();
6372 let val: f64 = rand::Rng::r#gen(&mut rng);
6373 Ok(VmValue::Float(val))
6374 }
6375 #[cfg(not(feature = "native"))]
6376 BuiltinId::Random => Err(runtime_err("random() not available in WASM")),
6377 #[cfg(feature = "native")]
6378 BuiltinId::RandomInt => {
6379 if args.len() < 2 {
6380 return Err(runtime_err("random_int() expects min and max"));
6381 }
6382 let a = match &args[0] {
6383 VmValue::Int(n) => *n,
6384 _ => return Err(runtime_err("random_int() expects integers")),
6385 };
6386 let b = match &args[1] {
6387 VmValue::Int(n) => *n,
6388 _ => return Err(runtime_err("random_int() expects integers")),
6389 };
6390 if a >= b {
6391 return Err(runtime_err("random_int() requires min < max"));
6392 }
6393 let mut rng = rand::thread_rng();
6394 let val: i64 = rand::Rng::gen_range(&mut rng, a..b);
6395 Ok(VmValue::Int(val))
6396 }
6397 #[cfg(not(feature = "native"))]
6398 BuiltinId::RandomInt => Err(runtime_err("random_int() not available in WASM")),
6399 #[cfg(feature = "native")]
6400 BuiltinId::Sample => {
6401 use rand::seq::SliceRandom;
6402 if args.is_empty() {
6403 return Err(runtime_err("sample() expects a list and count"));
6404 }
6405 let items = match &args[0] {
6406 VmValue::List(items) => items,
6407 _ => return Err(runtime_err("sample() expects a list")),
6408 };
6409 let k = match args.get(1) {
6410 Some(VmValue::Int(n)) => *n as usize,
6411 _ => 1,
6412 };
6413 if k > items.len() {
6414 return Err(runtime_err("sample() count exceeds list length"));
6415 }
6416 let mut rng = rand::thread_rng();
6417 let mut indices: Vec<usize> = (0..items.len()).collect();
6418 indices.partial_shuffle(&mut rng, k);
6419 let result: Vec<VmValue> = indices[..k].iter().map(|&i| items[i].clone()).collect();
6420 if k == 1 && args.get(1).is_none() {
6421 Ok(result.into_iter().next().unwrap_or(VmValue::None))
6422 } else {
6423 Ok(VmValue::List(Box::new(result)))
6424 }
6425 }
6426 #[cfg(not(feature = "native"))]
6427 BuiltinId::Sample => Err(runtime_err("sample() not available in WASM")),
6428
6429 BuiltinId::Exp => {
6431 let x = match args.first() {
6432 Some(VmValue::Float(f)) => *f,
6433 Some(VmValue::Int(n)) => *n as f64,
6434 _ => return Err(runtime_err("exp() expects a number")),
6435 };
6436 Ok(VmValue::Float(x.exp()))
6437 }
6438 BuiltinId::IsNan => {
6439 let result = match args.first() {
6440 Some(VmValue::Float(f)) => f.is_nan(),
6441 _ => false,
6442 };
6443 Ok(VmValue::Bool(result))
6444 }
6445 BuiltinId::IsInfinite => {
6446 let result = match args.first() {
6447 Some(VmValue::Float(f)) => f.is_infinite(),
6448 _ => false,
6449 };
6450 Ok(VmValue::Bool(result))
6451 }
6452 BuiltinId::Sign => match args.first() {
6453 Some(VmValue::Int(n)) => Ok(VmValue::Int(if *n > 0 {
6454 1
6455 } else if *n < 0 {
6456 -1
6457 } else {
6458 0
6459 })),
6460 Some(VmValue::Float(f)) => {
6461 if f.is_nan() {
6462 Ok(VmValue::Float(f64::NAN))
6463 } else if *f > 0.0 {
6464 Ok(VmValue::Int(1))
6465 } else if *f < 0.0 {
6466 Ok(VmValue::Int(-1))
6467 } else {
6468 Ok(VmValue::Int(0))
6469 }
6470 }
6471 _ => Err(runtime_err("sign() expects a number")),
6472 },
6473 #[cfg(feature = "native")]
6475 BuiltinId::AssertTableEq => {
6476 if args.len() < 2 {
6477 return Err(runtime_err("assert_table_eq() expects 2 table arguments"));
6478 }
6479 let t1 = match &args[0] {
6480 VmValue::Table(t) => t,
6481 _ => {
6482 return Err(runtime_err(
6483 "assert_table_eq() first argument must be a table",
6484 ));
6485 }
6486 };
6487 let t2 = match &args[1] {
6488 VmValue::Table(t) => t,
6489 _ => {
6490 return Err(runtime_err(
6491 "assert_table_eq() second argument must be a table",
6492 ));
6493 }
6494 };
6495 if t1.df.schema() != t2.df.schema() {
6497 return Err(runtime_err(format!(
6498 "assert_table_eq: schemas differ\n left: {:?}\n right: {:?}",
6499 t1.df.schema(),
6500 t2.df.schema()
6501 )));
6502 }
6503 let batches1 = self.engine().collect(t1.df.clone()).map_err(runtime_err)?;
6505 let batches2 = self.engine().collect(t2.df.clone()).map_err(runtime_err)?;
6506 let rows1: Vec<String> = batches1
6508 .iter()
6509 .flat_map(|b| {
6510 (0..b.num_rows()).map(move |r| {
6511 (0..b.num_columns())
6512 .map(|c| {
6513 let col = b.column(c);
6514 format!("{:?}", col.slice(r, 1))
6515 })
6516 .collect::<Vec<_>>()
6517 .join(",")
6518 })
6519 })
6520 .collect();
6521 let rows2: Vec<String> = batches2
6522 .iter()
6523 .flat_map(|b| {
6524 (0..b.num_rows()).map(move |r| {
6525 (0..b.num_columns())
6526 .map(|c| {
6527 let col = b.column(c);
6528 format!("{:?}", col.slice(r, 1))
6529 })
6530 .collect::<Vec<_>>()
6531 .join(",")
6532 })
6533 })
6534 .collect();
6535 if rows1.len() != rows2.len() {
6536 return Err(runtime_err(format!(
6537 "assert_table_eq: row count differs ({} vs {})",
6538 rows1.len(),
6539 rows2.len()
6540 )));
6541 }
6542 for (i, (r1, r2)) in rows1.iter().zip(rows2.iter()).enumerate() {
6543 if r1 != r2 {
6544 return Err(runtime_err(format!(
6545 "assert_table_eq: row {} differs\n left: {}\n right: {}",
6546 i, r1, r2
6547 )));
6548 }
6549 }
6550 Ok(VmValue::None)
6551 }
6552 #[cfg(not(feature = "native"))]
6553 BuiltinId::AssertTableEq => Err(runtime_err("assert_table_eq() not available in WASM")),
6554
6555 BuiltinId::Today => {
6557 use chrono::{Datelike, TimeZone};
6558 let now = chrono::Utc::now();
6559 let midnight = chrono::Utc
6560 .with_ymd_and_hms(now.year(), now.month(), now.day(), 0, 0, 0)
6561 .single()
6562 .ok_or_else(|| runtime_err("Failed to compute today"))?;
6563 Ok(VmValue::DateTime(midnight.timestamp_millis()))
6564 }
6565 BuiltinId::DateAdd => {
6566 if args.len() < 3 {
6567 return Err(runtime_err("date_add() expects datetime, amount, unit"));
6568 }
6569 let ms = match &args[0] {
6570 VmValue::DateTime(ms) => *ms,
6571 VmValue::Int(ms) => *ms,
6572 _ => return Err(runtime_err("date_add() first arg must be datetime")),
6573 };
6574 let amount = match &args[1] {
6575 VmValue::Int(n) => *n,
6576 _ => return Err(runtime_err("date_add() amount must be an integer")),
6577 };
6578 let unit = match &args[2] {
6579 VmValue::String(s) => s.as_ref(),
6580 _ => return Err(runtime_err("date_add() unit must be a string")),
6581 };
6582 let offset_ms = match unit {
6583 "second" | "seconds" => amount * 1000,
6584 "minute" | "minutes" => amount * 60 * 1000,
6585 "hour" | "hours" => amount * 3600 * 1000,
6586 "day" | "days" => amount * 86400 * 1000,
6587 "week" | "weeks" => amount * 7 * 86400 * 1000,
6588 _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6589 };
6590 Ok(VmValue::DateTime(ms + offset_ms))
6591 }
6592 BuiltinId::DateDiff => {
6593 if args.len() < 3 {
6594 return Err(runtime_err(
6595 "date_diff() expects datetime1, datetime2, unit",
6596 ));
6597 }
6598 let ms1 = match &args[0] {
6599 VmValue::DateTime(ms) => *ms,
6600 VmValue::Int(ms) => *ms,
6601 _ => return Err(runtime_err("date_diff() args must be datetimes")),
6602 };
6603 let ms2 = match &args[1] {
6604 VmValue::DateTime(ms) => *ms,
6605 VmValue::Int(ms) => *ms,
6606 _ => return Err(runtime_err("date_diff() args must be datetimes")),
6607 };
6608 let unit = match &args[2] {
6609 VmValue::String(s) => s.as_ref(),
6610 _ => return Err(runtime_err("date_diff() unit must be a string")),
6611 };
6612 let diff_ms = ms1 - ms2;
6613 let result = match unit {
6614 "second" | "seconds" => diff_ms / 1000,
6615 "minute" | "minutes" => diff_ms / (60 * 1000),
6616 "hour" | "hours" => diff_ms / (3600 * 1000),
6617 "day" | "days" => diff_ms / (86400 * 1000),
6618 "week" | "weeks" => diff_ms / (7 * 86400 * 1000),
6619 _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6620 };
6621 Ok(VmValue::Int(result))
6622 }
6623 BuiltinId::DateTrunc => {
6624 if args.len() < 2 {
6625 return Err(runtime_err("date_trunc() expects datetime and unit"));
6626 }
6627 let ms = match &args[0] {
6628 VmValue::DateTime(ms) => *ms,
6629 VmValue::Int(ms) => *ms,
6630 _ => return Err(runtime_err("date_trunc() first arg must be datetime")),
6631 };
6632 let unit = match &args[1] {
6633 VmValue::String(s) => s.as_ref(),
6634 _ => return Err(runtime_err("date_trunc() unit must be a string")),
6635 };
6636 use chrono::{Datelike, TimeZone, Timelike};
6637 let secs = ms / 1000;
6638 let dt = chrono::Utc
6639 .timestamp_opt(secs, 0)
6640 .single()
6641 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6642 let truncated = match unit {
6643 "second" => chrono::Utc
6644 .with_ymd_and_hms(
6645 dt.year(),
6646 dt.month(),
6647 dt.day(),
6648 dt.hour(),
6649 dt.minute(),
6650 dt.second(),
6651 )
6652 .single(),
6653 "minute" => chrono::Utc
6654 .with_ymd_and_hms(
6655 dt.year(),
6656 dt.month(),
6657 dt.day(),
6658 dt.hour(),
6659 dt.minute(),
6660 0,
6661 )
6662 .single(),
6663 "hour" => chrono::Utc
6664 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
6665 .single(),
6666 "day" => chrono::Utc
6667 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
6668 .single(),
6669 "month" => chrono::Utc
6670 .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
6671 .single(),
6672 "year" => chrono::Utc
6673 .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
6674 .single(),
6675 _ => return Err(runtime_err(format!("Unknown truncation unit: {unit}"))),
6676 };
6677 Ok(VmValue::DateTime(
6678 truncated
6679 .ok_or_else(|| runtime_err("Invalid truncation"))?
6680 .timestamp_millis(),
6681 ))
6682 }
6683 BuiltinId::DateExtract => {
6684 if args.len() < 2 {
6685 return Err(runtime_err("extract() expects datetime and part"));
6686 }
6687 let ms = match &args[0] {
6688 VmValue::DateTime(ms) => *ms,
6689 VmValue::Int(ms) => *ms,
6690 _ => return Err(runtime_err("extract() first arg must be datetime")),
6691 };
6692 let part = match &args[1] {
6693 VmValue::String(s) => s.as_ref(),
6694 _ => return Err(runtime_err("extract() part must be a string")),
6695 };
6696 use chrono::{Datelike, TimeZone, Timelike};
6697 let secs = ms / 1000;
6698 let dt = chrono::Utc
6699 .timestamp_opt(secs, 0)
6700 .single()
6701 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6702 let val = match part {
6703 "year" => dt.year() as i64,
6704 "month" => dt.month() as i64,
6705 "day" => dt.day() as i64,
6706 "hour" => dt.hour() as i64,
6707 "minute" => dt.minute() as i64,
6708 "second" => dt.second() as i64,
6709 "weekday" | "dow" => dt.weekday().num_days_from_monday() as i64,
6710 "day_of_year" | "doy" => dt.ordinal() as i64,
6711 _ => return Err(runtime_err(format!("Unknown date part: {part}"))),
6712 };
6713 Ok(VmValue::Int(val))
6714 }
6715
6716 #[cfg(feature = "mcp")]
6718 BuiltinId::McpConnect => {
6719 if args.is_empty() {
6720 return Err(runtime_err(
6721 "mcp_connect expects at least 1 argument: command or URL",
6722 ));
6723 }
6724 let command = match &args[0] {
6725 VmValue::String(s) => s.to_string(),
6726 _ => return Err(runtime_err("mcp_connect: first argument must be a string")),
6727 };
6728
6729 #[cfg(feature = "native")]
6731 let sampling_cb: Option<tl_mcp::SamplingCallback> =
6732 Some(Arc::new(|req: tl_mcp::SamplingRequest| {
6733 let model = req
6734 .model_hint
6735 .as_deref()
6736 .unwrap_or("claude-sonnet-4-20250514");
6737 let messages: Vec<serde_json::Value> = req
6738 .messages
6739 .iter()
6740 .map(|(role, content)| {
6741 serde_json::json!({"role": role, "content": content})
6742 })
6743 .collect();
6744 let response = tl_ai::chat_with_tools(
6745 model,
6746 req.system_prompt.as_deref(),
6747 &messages,
6748 &[], None, None, None, )
6753 .map_err(|e| format!("Sampling LLM error: {e}"))?;
6754 match response {
6755 tl_ai::LlmResponse::Text(text) => Ok(tl_mcp::SamplingResponse {
6756 model: model.to_string(),
6757 content: text,
6758 stop_reason: Some("endTurn".to_string()),
6759 }),
6760 tl_ai::LlmResponse::ToolUse(_) => {
6761 Err("Sampling does not support tool use".to_string())
6762 }
6763 }
6764 }));
6765
6766 #[cfg(not(feature = "native"))]
6767 let sampling_cb: Option<tl_mcp::SamplingCallback> = None;
6768
6769 let client = if command.starts_with("http://") || command.starts_with("https://") {
6771 tl_mcp::McpClient::connect_http_with_sampling(&command, sampling_cb)
6772 .map_err(|e| runtime_err(format!("mcp_connect (HTTP) failed: {e}")))?
6773 } else {
6774 let cmd_args: Vec<String> = args[1..]
6775 .iter()
6776 .map(|a| match a {
6777 VmValue::String(s) => s.to_string(),
6778 other => format!("{}", other),
6779 })
6780 .collect();
6781 tl_mcp::McpClient::connect_with_sampling(
6782 &command,
6783 &cmd_args,
6784 self.security_policy.as_ref(),
6785 sampling_cb,
6786 )
6787 .map_err(|e| runtime_err(format!("mcp_connect failed: {e}")))?
6788 };
6789 Ok(VmValue::McpClient(Arc::new(client)))
6790 }
6791 #[cfg(not(feature = "mcp"))]
6792 BuiltinId::McpConnect => {
6793 Err(runtime_err("MCP not available. Build with --features mcp"))
6794 }
6795
6796 #[cfg(feature = "mcp")]
6797 BuiltinId::McpListTools => {
6798 if args.is_empty() {
6799 return Err(runtime_err("mcp_list_tools expects 1 argument: client"));
6800 }
6801 match &args[0] {
6802 VmValue::McpClient(client) => {
6803 let tools = client
6804 .list_tools()
6805 .map_err(|e| runtime_err(format!("mcp_list_tools failed: {e}")))?;
6806 let tool_values: Vec<VmValue> = tools
6807 .iter()
6808 .map(|tool| {
6809 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6810 pairs.push((
6811 Arc::from("name"),
6812 VmValue::String(Arc::from(tool.name.as_ref())),
6813 ));
6814 if let Some(desc) = &tool.description {
6815 pairs.push((
6816 Arc::from("description"),
6817 VmValue::String(Arc::from(desc.as_ref())),
6818 ));
6819 }
6820 let schema_json = serde_json::to_string(tool.input_schema.as_ref())
6821 .unwrap_or_default();
6822 if !schema_json.is_empty() && schema_json != "{}" {
6823 pairs.push((
6824 Arc::from("input_schema"),
6825 VmValue::String(Arc::from(schema_json.as_str())),
6826 ));
6827 }
6828 VmValue::Map(Box::new(pairs))
6829 })
6830 .collect();
6831 Ok(VmValue::List(Box::new(tool_values)))
6832 }
6833 _ => Err(runtime_err(
6834 "mcp_list_tools: argument must be an mcp_client",
6835 )),
6836 }
6837 }
6838 #[cfg(not(feature = "mcp"))]
6839 BuiltinId::McpListTools => {
6840 Err(runtime_err("MCP not available. Build with --features mcp"))
6841 }
6842
6843 #[cfg(feature = "mcp")]
6844 BuiltinId::McpCallTool => {
6845 if args.len() < 2 {
6846 return Err(runtime_err(
6847 "mcp_call_tool expects 2-3 arguments: client, tool_name, [args]",
6848 ));
6849 }
6850 let client = match &args[0] {
6851 VmValue::McpClient(c) => c.clone(),
6852 _ => {
6853 return Err(runtime_err(
6854 "mcp_call_tool: first argument must be an mcp_client",
6855 ));
6856 }
6857 };
6858 let tool_name = match &args[1] {
6859 VmValue::String(s) => s.to_string(),
6860 _ => return Err(runtime_err("mcp_call_tool: tool_name must be a string")),
6861 };
6862 let arguments = if args.len() > 2 {
6863 vm_value_to_json(&args[2])
6864 } else {
6865 serde_json::Value::Object(serde_json::Map::new())
6866 };
6867 let result = client
6868 .call_tool(&tool_name, arguments)
6869 .map_err(|e| runtime_err(format!("mcp_call_tool failed: {e}")))?;
6870 let mut content_parts: Vec<VmValue> = Vec::new();
6871 for content in &result.content {
6872 if let Some(text) = content.as_text() {
6873 content_parts.push(VmValue::String(Arc::from(text.text.as_str())));
6874 }
6875 }
6876 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6877 if content_parts.len() == 1 {
6878 pairs.push((
6879 Arc::from("content"),
6880 content_parts.into_iter().next().unwrap(),
6881 ));
6882 } else {
6883 pairs.push((Arc::from("content"), VmValue::List(Box::new(content_parts))));
6884 }
6885 pairs.push((
6886 Arc::from("is_error"),
6887 VmValue::Bool(result.is_error.unwrap_or(false)),
6888 ));
6889 Ok(VmValue::Map(Box::new(pairs)))
6890 }
6891 #[cfg(not(feature = "mcp"))]
6892 BuiltinId::McpCallTool => {
6893 Err(runtime_err("MCP not available. Build with --features mcp"))
6894 }
6895
6896 #[cfg(feature = "mcp")]
6897 BuiltinId::McpDisconnect => {
6898 if args.is_empty() {
6899 return Err(runtime_err("mcp_disconnect expects 1 argument: client"));
6900 }
6901 match &args[0] {
6902 VmValue::McpClient(_) => Ok(VmValue::None),
6903 _ => Err(runtime_err(
6904 "mcp_disconnect: argument must be an mcp_client",
6905 )),
6906 }
6907 }
6908 #[cfg(not(feature = "mcp"))]
6909 BuiltinId::McpDisconnect => {
6910 Err(runtime_err("MCP not available. Build with --features mcp"))
6911 }
6912
6913 #[cfg(feature = "mcp")]
6914 BuiltinId::McpPing => {
6915 if args.is_empty() {
6916 return Err(runtime_err("mcp_ping expects 1 argument: client"));
6917 }
6918 match &args[0] {
6919 VmValue::McpClient(client) => {
6920 client
6921 .ping()
6922 .map_err(|e| runtime_err(format!("mcp_ping failed: {e}")))?;
6923 Ok(VmValue::Bool(true))
6924 }
6925 _ => Err(runtime_err("mcp_ping: argument must be an mcp_client")),
6926 }
6927 }
6928 #[cfg(not(feature = "mcp"))]
6929 BuiltinId::McpPing => Err(runtime_err("MCP not available. Build with --features mcp")),
6930
6931 #[cfg(feature = "mcp")]
6932 BuiltinId::McpServerInfo => {
6933 if args.is_empty() {
6934 return Err(runtime_err("mcp_server_info expects 1 argument: client"));
6935 }
6936 match &args[0] {
6937 VmValue::McpClient(client) => match client.server_info() {
6938 Some(info) => {
6939 let pairs: Vec<(Arc<str>, VmValue)> = vec![
6940 (
6941 Arc::from("name"),
6942 VmValue::String(Arc::from(info.server_info.name.as_str())),
6943 ),
6944 (
6945 Arc::from("version"),
6946 VmValue::String(Arc::from(info.server_info.version.as_str())),
6947 ),
6948 ];
6949 Ok(VmValue::Map(Box::new(pairs)))
6950 }
6951 None => Ok(VmValue::None),
6952 },
6953 _ => Err(runtime_err(
6954 "mcp_server_info: argument must be an mcp_client",
6955 )),
6956 }
6957 }
6958 #[cfg(not(feature = "mcp"))]
6959 BuiltinId::McpServerInfo => {
6960 Err(runtime_err("MCP not available. Build with --features mcp"))
6961 }
6962
6963 #[cfg(feature = "mcp")]
6964 BuiltinId::McpServe => {
6965 self.check_permission("network")?;
6966 if args.is_empty() {
6967 return Err(runtime_err(
6968 "mcp_serve expects 1 argument: list of tool definitions",
6969 ));
6970 }
6971 let tool_list = match &args[0] {
6972 VmValue::List(items) => items.as_ref().clone(),
6973 _ => {
6974 return Err(runtime_err(
6975 "mcp_serve: argument must be a list of tool maps",
6976 ));
6977 }
6978 };
6979
6980 let mut channel_tools = Vec::new();
6982 let mut tool_handlers: HashMap<String, VmValue> = HashMap::new();
6983
6984 for item in &tool_list {
6985 let pairs = match item {
6986 VmValue::Map(p) => p.as_ref(),
6987 _ => {
6988 return Err(runtime_err(
6989 "mcp_serve: each tool must be a map with name, description, handler",
6990 ));
6991 }
6992 };
6993 let mut name = String::new();
6994 let mut description = String::new();
6995 let mut handler = None;
6996 let mut input_schema = serde_json::json!({"type": "object"});
6997
6998 for (k, v) in pairs {
6999 match k.as_ref() {
7000 "name" => {
7001 if let VmValue::String(s) = v {
7002 name = s.to_string();
7003 }
7004 }
7005 "description" => {
7006 if let VmValue::String(s) = v {
7007 description = s.to_string();
7008 }
7009 }
7010 "handler" => {
7011 handler = Some(v.clone());
7012 }
7013 "input_schema" | "parameters" => {
7014 if let VmValue::String(s) = v
7015 && let Ok(parsed) =
7016 serde_json::from_str::<serde_json::Value>(s.as_ref())
7017 {
7018 input_schema = parsed;
7019 }
7020 }
7021 _ => {}
7022 }
7023 }
7024
7025 if name.is_empty() {
7026 return Err(runtime_err("mcp_serve: tool missing 'name'"));
7027 }
7028 if let Some(h) = handler {
7029 tool_handlers.insert(name.clone(), h);
7030 }
7031
7032 channel_tools.push(tl_mcp::server::ChannelToolDef {
7033 name,
7034 description,
7035 input_schema,
7036 });
7037 }
7038
7039 let (builder, rx) = tl_mcp::server::TlServerHandler::builder()
7041 .name("tl-mcp-server")
7042 .version("1.0.0")
7043 .channel_tools(channel_tools);
7044 let server_handler = builder.build();
7045
7046 let _server_handle = tl_mcp::server::serve_stdio_background(server_handler);
7048
7049 while let Ok(req) = rx.recv() {
7051 let result = if let Some(func) = tool_handlers.get(&req.tool_name) {
7052 let call_args = self.json_to_vm_args(&req.arguments);
7054 match self.call_value(func.clone(), &call_args) {
7055 Ok(val) => {
7056 Ok(serde_json::json!(format!("{val}")))
7058 }
7059 Err(e) => Err(format!("{e}")),
7060 }
7061 } else {
7062 Err(format!("Unknown tool: {}", req.tool_name))
7063 };
7064 let _ = req.response_tx.send(result);
7065 }
7066
7067 Ok(VmValue::None)
7068 }
7069 #[cfg(not(feature = "mcp"))]
7070 BuiltinId::McpServe => Err(runtime_err("MCP not available. Build with --features mcp")),
7071
7072 #[cfg(feature = "mcp")]
7074 BuiltinId::McpListResources => {
7075 if args.is_empty() {
7076 return Err(runtime_err("mcp_list_resources expects 1 argument: client"));
7077 }
7078 match &args[0] {
7079 VmValue::McpClient(client) => {
7080 let resources = client
7081 .list_resources()
7082 .map_err(|e| runtime_err(format!("mcp_list_resources failed: {e}")))?;
7083 let vals: Vec<VmValue> = resources
7084 .iter()
7085 .map(|r| {
7086 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7087 pairs.push((
7088 Arc::from("uri"),
7089 VmValue::String(Arc::from(r.uri.as_str())),
7090 ));
7091 pairs.push((
7092 Arc::from("name"),
7093 VmValue::String(Arc::from(r.name.as_str())),
7094 ));
7095 if let Some(desc) = &r.description {
7096 pairs.push((
7097 Arc::from("description"),
7098 VmValue::String(Arc::from(desc.as_str())),
7099 ));
7100 }
7101 if let Some(mime) = &r.mime_type {
7102 pairs.push((
7103 Arc::from("mime_type"),
7104 VmValue::String(Arc::from(mime.as_str())),
7105 ));
7106 }
7107 VmValue::Map(Box::new(pairs))
7108 })
7109 .collect();
7110 Ok(VmValue::List(Box::new(vals)))
7111 }
7112 _ => Err(runtime_err(
7113 "mcp_list_resources: argument must be an mcp_client",
7114 )),
7115 }
7116 }
7117 #[cfg(not(feature = "mcp"))]
7118 BuiltinId::McpListResources => {
7119 Err(runtime_err("MCP not available. Build with --features mcp"))
7120 }
7121
7122 #[cfg(feature = "mcp")]
7123 BuiltinId::McpReadResource => {
7124 if args.len() < 2 {
7125 return Err(runtime_err(
7126 "mcp_read_resource expects 2 arguments: client, uri",
7127 ));
7128 }
7129 let client = match &args[0] {
7130 VmValue::McpClient(c) => c.clone(),
7131 _ => {
7132 return Err(runtime_err(
7133 "mcp_read_resource: first argument must be an mcp_client",
7134 ));
7135 }
7136 };
7137 let uri = match &args[1] {
7138 VmValue::String(s) => s.to_string(),
7139 _ => return Err(runtime_err("mcp_read_resource: uri must be a string")),
7140 };
7141 let result = client
7142 .read_resource(&uri)
7143 .map_err(|e| runtime_err(format!("mcp_read_resource failed: {e}")))?;
7144 let contents: Vec<VmValue> = result
7146 .contents
7147 .iter()
7148 .map(|content| {
7149 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7150 let json = serde_json::to_value(content).unwrap_or_default();
7151 if let Some(uri_s) = json.get("uri").and_then(|v| v.as_str()) {
7152 pairs.push((Arc::from("uri"), VmValue::String(Arc::from(uri_s))));
7153 }
7154 if let Some(mime) = json.get("mimeType").and_then(|v| v.as_str()) {
7155 pairs.push((Arc::from("mime_type"), VmValue::String(Arc::from(mime))));
7156 }
7157 if let Some(text) = json.get("text").and_then(|v| v.as_str()) {
7158 pairs.push((Arc::from("text"), VmValue::String(Arc::from(text))));
7159 }
7160 if let Some(blob) = json.get("blob").and_then(|v| v.as_str()) {
7161 pairs.push((Arc::from("blob"), VmValue::String(Arc::from(blob))));
7162 }
7163 VmValue::Map(Box::new(pairs))
7164 })
7165 .collect();
7166 if contents.len() == 1 {
7167 Ok(contents.into_iter().next().unwrap())
7168 } else {
7169 Ok(VmValue::List(Box::new(contents)))
7170 }
7171 }
7172 #[cfg(not(feature = "mcp"))]
7173 BuiltinId::McpReadResource => {
7174 Err(runtime_err("MCP not available. Build with --features mcp"))
7175 }
7176
7177 #[cfg(feature = "mcp")]
7178 BuiltinId::McpListPrompts => {
7179 if args.is_empty() {
7180 return Err(runtime_err("mcp_list_prompts expects 1 argument: client"));
7181 }
7182 match &args[0] {
7183 VmValue::McpClient(client) => {
7184 let prompts = client
7185 .list_prompts()
7186 .map_err(|e| runtime_err(format!("mcp_list_prompts failed: {e}")))?;
7187 let vals: Vec<VmValue> = prompts
7188 .iter()
7189 .map(|p| {
7190 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7191 pairs.push((
7192 Arc::from("name"),
7193 VmValue::String(Arc::from(p.name.as_str())),
7194 ));
7195 if let Some(desc) = &p.description {
7196 pairs.push((
7197 Arc::from("description"),
7198 VmValue::String(Arc::from(desc.as_str())),
7199 ));
7200 }
7201 if let Some(prompt_args) = &p.arguments {
7202 let arg_vals: Vec<VmValue> = prompt_args
7203 .iter()
7204 .map(|a| {
7205 let mut arg_pairs: Vec<(Arc<str>, VmValue)> =
7206 Vec::new();
7207 arg_pairs.push((
7208 Arc::from("name"),
7209 VmValue::String(Arc::from(a.name.as_str())),
7210 ));
7211 if let Some(desc) = &a.description {
7212 arg_pairs.push((
7213 Arc::from("description"),
7214 VmValue::String(Arc::from(desc.as_str())),
7215 ));
7216 }
7217 arg_pairs.push((
7218 Arc::from("required"),
7219 VmValue::Bool(a.required.unwrap_or(false)),
7220 ));
7221 VmValue::Map(Box::new(arg_pairs))
7222 })
7223 .collect();
7224 pairs.push((
7225 Arc::from("arguments"),
7226 VmValue::List(Box::new(arg_vals)),
7227 ));
7228 }
7229 VmValue::Map(Box::new(pairs))
7230 })
7231 .collect();
7232 Ok(VmValue::List(Box::new(vals)))
7233 }
7234 _ => Err(runtime_err(
7235 "mcp_list_prompts: argument must be an mcp_client",
7236 )),
7237 }
7238 }
7239 #[cfg(not(feature = "mcp"))]
7240 BuiltinId::McpListPrompts => {
7241 Err(runtime_err("MCP not available. Build with --features mcp"))
7242 }
7243
7244 #[cfg(feature = "mcp")]
7245 BuiltinId::McpGetPrompt => {
7246 if args.len() < 2 {
7247 return Err(runtime_err(
7248 "mcp_get_prompt expects 2-3 arguments: client, name, [args]",
7249 ));
7250 }
7251 let client = match &args[0] {
7252 VmValue::McpClient(c) => c.clone(),
7253 _ => {
7254 return Err(runtime_err(
7255 "mcp_get_prompt: first argument must be an mcp_client",
7256 ));
7257 }
7258 };
7259 let name = match &args[1] {
7260 VmValue::String(s) => s.to_string(),
7261 _ => return Err(runtime_err("mcp_get_prompt: name must be a string")),
7262 };
7263 let prompt_args = if args.len() > 2 {
7264 let json = vm_value_to_json(&args[2]);
7265 json.as_object().cloned()
7266 } else {
7267 None
7268 };
7269 let result = client
7270 .get_prompt(&name, prompt_args)
7271 .map_err(|e| runtime_err(format!("mcp_get_prompt failed: {e}")))?;
7272 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7273 if let Some(desc) = &result.description {
7274 pairs.push((
7275 Arc::from("description"),
7276 VmValue::String(Arc::from(desc.as_str())),
7277 ));
7278 }
7279 let messages: Vec<VmValue> = result
7281 .messages
7282 .iter()
7283 .map(|m| {
7284 let mut msg_pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7285 let msg_json = serde_json::to_value(m).unwrap_or_default();
7286 if let Some(role) = msg_json.get("role").and_then(|v| v.as_str()) {
7288 msg_pairs.push((Arc::from("role"), VmValue::String(Arc::from(role))));
7289 }
7290 if let Some(content) = msg_json.get("content") {
7292 if let Some(text) = content.get("text").and_then(|v| v.as_str()) {
7293 msg_pairs
7294 .push((Arc::from("content"), VmValue::String(Arc::from(text))));
7295 } else {
7296 let content_str = content.to_string();
7297 msg_pairs.push((
7298 Arc::from("content"),
7299 VmValue::String(Arc::from(content_str.as_str())),
7300 ));
7301 }
7302 }
7303 VmValue::Map(Box::new(msg_pairs))
7304 })
7305 .collect();
7306 pairs.push((Arc::from("messages"), VmValue::List(Box::new(messages))));
7307 Ok(VmValue::Map(Box::new(pairs)))
7308 }
7309 #[cfg(not(feature = "mcp"))]
7310 BuiltinId::McpGetPrompt => {
7311 Err(runtime_err("MCP not available. Build with --features mcp"))
7312 }
7313 }
7314 }
7315
7316 fn vmvalue_to_f64_list(&self, val: &VmValue) -> Result<Vec<f64>, TlError> {
7319 match val {
7320 VmValue::List(items) => items
7321 .iter()
7322 .map(|item| match item {
7323 VmValue::Int(n) => Ok(*n as f64),
7324 VmValue::Float(f) => Ok(*f),
7325 _ => Err(runtime_err("Expected number in list")),
7326 })
7327 .collect(),
7328 VmValue::Int(n) => Ok(vec![*n as f64]),
7329 VmValue::Float(f) => Ok(vec![*f]),
7330 _ => Err(runtime_err("Expected a list of numbers")),
7331 }
7332 }
7333
7334 fn vmvalue_to_usize_list(&self, val: &VmValue) -> Result<Vec<usize>, TlError> {
7335 match val {
7336 VmValue::List(items) => items
7337 .iter()
7338 .map(|item| match item {
7339 VmValue::Int(n) => Ok(*n as usize),
7340 _ => Err(runtime_err("Expected integer in shape list")),
7341 })
7342 .collect(),
7343 _ => Err(runtime_err("Expected a list of integers for shape")),
7344 }
7345 }
7346
7347 #[cfg(feature = "native")]
7348 fn handle_train(
7349 &mut self,
7350 frame_idx: usize,
7351 algo_const: u8,
7352 config_const: u8,
7353 ) -> Result<VmValue, TlError> {
7354 let frame = &self.frames[frame_idx];
7355 let algorithm = match &frame.prototype.constants[algo_const as usize] {
7356 Constant::String(s) => s.to_string(),
7357 _ => return Err(runtime_err("Expected string constant for algorithm")),
7358 };
7359 let config_args = match &frame.prototype.constants[config_const as usize] {
7360 Constant::AstExprList(args) => args.clone(),
7361 _ => return Err(runtime_err("Expected AST expr list for train config")),
7362 };
7363
7364 let mut data_val = None;
7366 let mut target_name = None;
7367 let mut feature_names: Vec<String> = Vec::new();
7368
7369 for arg in &config_args {
7370 if let AstExpr::NamedArg { name, value } = arg {
7371 match name.as_str() {
7372 "data" => {
7373 data_val = Some(self.eval_ast_to_vm(value)?);
7374 }
7375 "target" => {
7376 if let AstExpr::String(s) = value.as_ref() {
7377 target_name = Some(s.clone());
7378 }
7379 }
7380 "features" => {
7381 if let AstExpr::List(items) = value.as_ref() {
7382 for item in items {
7383 if let AstExpr::String(s) = item {
7384 feature_names.push(s.clone());
7385 }
7386 }
7387 }
7388 }
7389 _ => {}
7390 }
7391 }
7392 }
7393
7394 let table = match data_val {
7396 Some(VmValue::Table(t)) => t,
7397 _ => return Err(runtime_err("train: data must be a table")),
7398 };
7399 let target = target_name.ok_or_else(|| runtime_err("train: target is required"))?;
7400
7401 let batches = self.engine().collect(table.df).map_err(runtime_err)?;
7403 if batches.is_empty() {
7404 return Err(runtime_err("train: empty dataset"));
7405 }
7406
7407 let batch = &batches[0];
7409 let schema = batch.schema();
7410 if feature_names.is_empty() {
7411 for field in schema.fields() {
7412 if field.name() != &target {
7413 feature_names.push(field.name().clone());
7414 }
7415 }
7416 }
7417
7418 let n_rows = batch.num_rows();
7420 let n_features = feature_names.len();
7421 let mut features_data = Vec::with_capacity(n_rows * n_features);
7422 let mut target_data = Vec::with_capacity(n_rows);
7423
7424 for col_name in &feature_names {
7425 let col_idx = schema
7426 .index_of(col_name)
7427 .map_err(|_| runtime_err(format!("Column not found: {col_name}")))?;
7428 let col_arr = batch.column(col_idx);
7429 Self::extract_f64_column(col_arr, &mut features_data)?;
7430 }
7431
7432 let target_idx = schema
7434 .index_of(&target)
7435 .map_err(|_| runtime_err(format!("Target column not found: {target}")))?;
7436 let target_arr = batch.column(target_idx);
7437 Self::extract_f64_column(target_arr, &mut target_data)?;
7438
7439 let mut row_major = Vec::with_capacity(n_rows * n_features);
7441 for row in 0..n_rows {
7442 for col in 0..n_features {
7443 row_major.push(features_data[col * n_rows + row]);
7444 }
7445 }
7446
7447 let features_tensor = tl_ai::TlTensor::from_vec(row_major, &[n_rows, n_features])
7448 .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7449 let target_tensor = tl_ai::TlTensor::from_vec(target_data, &[n_rows])
7450 .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7451
7452 let config = tl_ai::TrainConfig {
7453 features: features_tensor,
7454 target: target_tensor,
7455 feature_names: feature_names.clone(),
7456 target_name: target.clone(),
7457 model_name: algorithm.clone(),
7458 split_ratio: 0.8,
7459 hyperparams: std::collections::HashMap::new(),
7460 };
7461
7462 let model = tl_ai::train(&algorithm, &config)
7463 .map_err(|e| runtime_err(format!("Training failed: {e}")))?;
7464
7465 Ok(VmValue::Model(Arc::new(model)))
7466 }
7467
7468 #[cfg(feature = "native")]
7469 fn extract_f64_column(
7470 col: &std::sync::Arc<dyn tl_data::datafusion::arrow::array::Array>,
7471 out: &mut Vec<f64>,
7472 ) -> Result<(), TlError> {
7473 use tl_data::datafusion::arrow::array::{
7474 Array, Float32Array, Float64Array, Int32Array, Int64Array,
7475 };
7476 let len = col.len();
7477 if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
7478 for i in 0..len {
7479 out.push(if arr.is_null(i) { 0.0 } else { arr.value(i) });
7480 }
7481 } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
7482 for i in 0..len {
7483 out.push(if arr.is_null(i) {
7484 0.0
7485 } else {
7486 arr.value(i) as f64
7487 });
7488 }
7489 } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
7490 for i in 0..len {
7491 out.push(if arr.is_null(i) {
7492 0.0
7493 } else {
7494 arr.value(i) as f64
7495 });
7496 }
7497 } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
7498 for i in 0..len {
7499 out.push(if arr.is_null(i) {
7500 0.0
7501 } else {
7502 arr.value(i) as f64
7503 });
7504 }
7505 } else {
7506 return Err(runtime_err(
7507 "Column must be numeric (int32, int64, float32, float64)",
7508 ));
7509 }
7510 Ok(())
7511 }
7512
7513 #[cfg(feature = "native")]
7514 fn handle_pipeline_exec(
7515 &mut self,
7516 frame_idx: usize,
7517 name_const: u8,
7518 config_const: u8,
7519 ) -> Result<VmValue, TlError> {
7520 let frame = &self.frames[frame_idx];
7521 let name = match &frame.prototype.constants[name_const as usize] {
7522 Constant::String(s) => s.to_string(),
7523 _ => return Err(runtime_err("Expected string constant for pipeline name")),
7524 };
7525
7526 let mut schedule = None;
7527 let mut timeout_ms = None;
7528 let mut retries = 0u32;
7529
7530 if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7531 for arg in args {
7532 if let AstExpr::NamedArg { name: key, value } = arg {
7533 match key.as_str() {
7534 "schedule" => {
7535 if let AstExpr::String(s) = value.as_ref() {
7536 schedule = Some(s.clone());
7537 }
7538 }
7539 "timeout" => {
7540 if let AstExpr::String(s) = value.as_ref() {
7541 timeout_ms = tl_stream::parse_duration(s).ok();
7542 }
7543 }
7544 "retries" => {
7545 if let AstExpr::Int(n) = value.as_ref() {
7546 retries = *n as u32;
7547 }
7548 }
7549 _ => {}
7550 }
7551 }
7552 }
7553 }
7554
7555 let def = tl_stream::PipelineDef {
7556 name,
7557 schedule,
7558 timeout_ms,
7559 retries,
7560 };
7561
7562 self.output
7563 .push(format!("Pipeline '{}': success", def.name));
7564 Ok(VmValue::PipelineDef(Arc::new(def)))
7565 }
7566
7567 #[cfg(feature = "native")]
7568 fn handle_stream_exec(
7569 &mut self,
7570 frame_idx: usize,
7571 config_const: u8,
7572 ) -> Result<VmValue, TlError> {
7573 let frame = &self.frames[frame_idx];
7574 let config_args = match &frame.prototype.constants[config_const as usize] {
7575 Constant::AstExprList(args) => args.clone(),
7576 _ => return Err(runtime_err("Expected AST expr list for stream config")),
7577 };
7578
7579 let mut name = String::new();
7580 let mut window = None;
7581 let mut watermark_ms = None;
7582
7583 for arg in &config_args {
7584 if let AstExpr::NamedArg { name: key, value } = arg {
7585 match key.as_str() {
7586 "name" => {
7587 if let AstExpr::String(s) = value.as_ref() {
7588 name = s.clone();
7589 }
7590 }
7591 "window" => {
7592 if let AstExpr::String(s) = value.as_ref() {
7593 window = Self::parse_window_type(s);
7594 }
7595 }
7596 "watermark" => {
7597 if let AstExpr::String(s) = value.as_ref() {
7598 watermark_ms = tl_stream::parse_duration(s).ok();
7599 }
7600 }
7601 _ => {}
7602 }
7603 }
7604 }
7605
7606 let def = tl_stream::StreamDef {
7607 name: name.clone(),
7608 window,
7609 watermark_ms,
7610 };
7611
7612 self.output.push(format!("Stream '{}' declared", name));
7613 Ok(VmValue::StreamDef(Arc::new(def)))
7614 }
7615
7616 #[cfg(feature = "native")]
7617 fn handle_agent_exec(
7618 &mut self,
7619 frame_idx: usize,
7620 name_const: u8,
7621 config_const: u8,
7622 ) -> Result<VmValue, TlError> {
7623 let frame = &self.frames[frame_idx];
7624 let name = match &frame.prototype.constants[name_const as usize] {
7625 Constant::String(s) => s.to_string(),
7626 _ => return Err(runtime_err("Expected string constant for agent name")),
7627 };
7628
7629 let mut model = String::new();
7630 let mut system_prompt = None;
7631 let mut max_turns = 10u32;
7632 let mut temperature = None;
7633 let mut max_tokens = None;
7634 let mut base_url = None;
7635 let mut api_key = None;
7636 let mut output_format = None;
7637 let mut tools = Vec::new();
7638 #[cfg(feature = "mcp")]
7639 let mut mcp_clients: Vec<Arc<tl_mcp::McpClient>> = Vec::new();
7640
7641 if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7642 for arg in args {
7643 if let AstExpr::NamedArg { name: key, value } = arg {
7644 if let Some(tool_name) = key.strip_prefix("tool:") {
7645 let (desc, params) = Self::extract_tool_from_ast(value);
7647 tools.push(tl_stream::AgentTool {
7648 name: tool_name.to_string(),
7649 description: desc,
7650 parameters: params,
7651 });
7652 } else if key.starts_with("mcp_server:") {
7653 #[cfg(feature = "mcp")]
7655 if let AstExpr::Ident(var_name) = value.as_ref()
7656 && let Some(VmValue::McpClient(client)) = self.globals.get(var_name)
7657 {
7658 mcp_clients.push(client.clone());
7659 }
7660 } else {
7661 match key.as_str() {
7662 "model" => {
7663 if let AstExpr::String(s) = value.as_ref() {
7664 model = s.clone();
7665 }
7666 }
7667 "system" => {
7668 if let AstExpr::String(s) = value.as_ref() {
7669 system_prompt = Some(s.clone());
7670 }
7671 }
7672 "max_turns" => {
7673 if let AstExpr::Int(n) = value.as_ref() {
7674 max_turns = *n as u32;
7675 }
7676 }
7677 "temperature" => {
7678 if let AstExpr::Float(f) = value.as_ref() {
7679 temperature = Some(*f);
7680 }
7681 }
7682 "max_tokens" => {
7683 if let AstExpr::Int(n) = value.as_ref() {
7684 max_tokens = Some(*n as u32);
7685 }
7686 }
7687 "base_url" => {
7688 if let AstExpr::String(s) = value.as_ref() {
7689 base_url = Some(s.clone());
7690 }
7691 }
7692 "api_key" => {
7693 if let AstExpr::String(s) = value.as_ref() {
7694 api_key = Some(s.clone());
7695 }
7696 }
7697 "output_format" => {
7698 if let AstExpr::String(s) = value.as_ref() {
7699 output_format = Some(s.clone());
7700 }
7701 }
7702 _ => {}
7703 }
7704 }
7705 }
7706 }
7707 }
7708
7709 let def = tl_stream::AgentDef {
7710 name: name.clone(),
7711 model,
7712 system_prompt,
7713 tools,
7714 max_turns,
7715 temperature,
7716 max_tokens,
7717 base_url,
7718 api_key,
7719 output_format,
7720 };
7721
7722 #[cfg(feature = "mcp")]
7724 if !mcp_clients.is_empty() {
7725 self.mcp_agent_clients.insert(name.clone(), mcp_clients);
7726 }
7727
7728 Ok(VmValue::AgentDef(Arc::new(def)))
7729 }
7730
7731 #[cfg(feature = "native")]
7732 fn extract_tool_from_ast(expr: &AstExpr) -> (String, serde_json::Value) {
7733 let mut desc = String::new();
7734 let mut params = serde_json::Value::Object(serde_json::Map::new());
7735 if let AstExpr::Map(pairs) = expr {
7736 for (key_expr, val_expr) in pairs {
7737 if let AstExpr::Ident(key) | AstExpr::String(key) = key_expr {
7738 match key.as_str() {
7739 "description" => {
7740 if let AstExpr::String(s) = val_expr {
7741 desc = s.clone();
7742 }
7743 }
7744 "parameters" => {
7745 params = Self::ast_to_json(val_expr);
7746 }
7747 _ => {}
7748 }
7749 }
7750 }
7751 }
7752 (desc, params)
7753 }
7754
7755 #[cfg(feature = "native")]
7756 fn ast_to_json(expr: &AstExpr) -> serde_json::Value {
7757 match expr {
7758 AstExpr::String(s) => serde_json::Value::String(s.clone()),
7759 AstExpr::Int(n) => serde_json::json!(*n),
7760 AstExpr::Float(f) => serde_json::json!(*f),
7761 AstExpr::Bool(b) => serde_json::Value::Bool(*b),
7762 AstExpr::None => serde_json::Value::Null,
7763 AstExpr::List(items) => {
7764 serde_json::Value::Array(items.iter().map(Self::ast_to_json).collect())
7765 }
7766 AstExpr::Map(pairs) => {
7767 let mut map = serde_json::Map::new();
7768 for (k, v) in pairs {
7769 let key = match k {
7770 AstExpr::String(s) | AstExpr::Ident(s) => s.clone(),
7771 _ => format!("{k:?}"),
7772 };
7773 map.insert(key, Self::ast_to_json(v));
7774 }
7775 serde_json::Value::Object(map)
7776 }
7777 _ => serde_json::Value::Null,
7778 }
7779 }
7780
7781 #[cfg(feature = "native")]
7782 fn exec_agent_loop(
7783 &mut self,
7784 agent_def: &tl_stream::AgentDef,
7785 user_message: &str,
7786 history: Option<&[(String, String)]>,
7787 ) -> Result<VmValue, TlError> {
7788 use tl_ai::{LlmResponse, chat_with_tools, format_tool_result_messages};
7789
7790 let model = &agent_def.model;
7791 let system = agent_def.system_prompt.as_deref();
7792 let base_url = agent_def.base_url.as_deref();
7793 let api_key = agent_def.api_key.as_deref();
7794
7795 let provider = if model.starts_with("claude") {
7796 "anthropic"
7797 } else {
7798 "openai"
7799 };
7800
7801 #[allow(unused_mut)]
7803 let mut tools_json: Vec<serde_json::Value> = agent_def
7804 .tools
7805 .iter()
7806 .map(|t| {
7807 serde_json::json!({
7808 "type": "function",
7809 "function": {
7810 "name": t.name,
7811 "description": t.description,
7812 "parameters": t.parameters
7813 }
7814 })
7815 })
7816 .collect();
7817
7818 #[cfg(feature = "mcp")]
7820 let mcp_clients = self
7821 .mcp_agent_clients
7822 .get(&agent_def.name)
7823 .cloned()
7824 .unwrap_or_default();
7825 #[cfg(feature = "mcp")]
7826 let mcp_tool_dispatch: std::collections::HashMap<String, usize> = {
7827 let mut dispatch = std::collections::HashMap::new();
7828 for (client_idx, client) in mcp_clients.iter().enumerate() {
7829 if let Ok(mcp_tools) = client.list_tools() {
7830 for tool in mcp_tools {
7831 let tool_name = tool.name.to_string();
7832 tools_json.push(serde_json::json!({
7833 "type": "function",
7834 "function": {
7835 "name": &tool_name,
7836 "description": tool.description.as_deref().unwrap_or(""),
7837 "parameters": serde_json::Value::Object((*tool.input_schema).clone())
7838 }
7839 }));
7840 dispatch.insert(tool_name, client_idx);
7841 }
7842 }
7843 }
7844 dispatch
7845 };
7846
7847 let mut messages: Vec<serde_json::Value> = Vec::new();
7849 if let Some(hist) = history {
7850 for (role, content) in hist {
7851 messages.push(serde_json::json!({"role": role, "content": content}));
7852 }
7853 }
7854 messages.push(serde_json::json!({
7856 "role": "user",
7857 "content": user_message
7858 }));
7859
7860 for turn in 0..agent_def.max_turns {
7861 let response = chat_with_tools(
7862 model,
7863 system,
7864 &messages,
7865 &tools_json,
7866 base_url,
7867 api_key,
7868 agent_def.output_format.as_deref(),
7869 )
7870 .map_err(|e| runtime_err(format!("Agent LLM error: {e}")))?;
7871
7872 match response {
7873 LlmResponse::Text(text) => {
7874 messages.push(serde_json::json!({"role": "assistant", "content": &text}));
7876
7877 let history_list: Vec<VmValue> = messages
7879 .iter()
7880 .filter_map(|m| {
7881 let role = m["role"].as_str()?;
7882 let content = m["content"].as_str()?;
7883 Some(VmValue::List(Box::new(vec![
7884 VmValue::String(Arc::from(role)),
7885 VmValue::String(Arc::from(content)),
7886 ])))
7887 })
7888 .collect();
7889
7890 let result = VmValue::Map(Box::new(vec![
7892 (
7893 Arc::from("response"),
7894 VmValue::String(Arc::from(text.as_str())),
7895 ),
7896 (Arc::from("turns"), VmValue::Int(turn as i64 + 1)),
7897 (Arc::from("history"), VmValue::List(Box::new(history_list))),
7898 ]));
7899
7900 let hook_name = format!("__agent_{}_on_complete__", agent_def.name);
7902 if let Some(hook) = self.globals.get(&hook_name).cloned() {
7903 let _ = self.call_value(hook, std::slice::from_ref(&result));
7904 }
7905
7906 return Ok(result);
7907 }
7908 LlmResponse::ToolUse(tool_calls) => {
7909 let tc_json: Vec<serde_json::Value> = tool_calls
7911 .iter()
7912 .map(|tc| {
7913 serde_json::json!({
7914 "id": tc.id,
7915 "type": "function",
7916 "function": {
7917 "name": tc.name,
7918 "arguments": serde_json::to_string(&tc.input).unwrap_or_default()
7919 }
7920 })
7921 })
7922 .collect();
7923 messages.push(serde_json::json!({
7924 "role": "assistant",
7925 "tool_calls": tc_json
7926 }));
7927
7928 #[allow(unused_mut)]
7930 let mut declared: Vec<String> =
7931 agent_def.tools.iter().map(|t| t.name.clone()).collect();
7932 #[cfg(feature = "mcp")]
7933 {
7934 for name in mcp_tool_dispatch.keys() {
7935 declared.push(name.clone());
7936 }
7937 }
7938
7939 let mut results: Vec<(String, String)> = Vec::new();
7941 for tc in &tool_calls {
7942 if !declared.iter().any(|d| d == &tc.name) {
7943 results.push((
7944 tc.name.clone(),
7945 format!("Error: '{}' not in declared tools", tc.name),
7946 ));
7947 continue;
7948 }
7949
7950 let result_str;
7952 #[cfg(feature = "mcp")]
7953 {
7954 if let Some(&client_idx) = mcp_tool_dispatch.get(tc.name.as_str()) {
7955 let mcp_result = mcp_clients[client_idx]
7956 .call_tool(&tc.name, tc.input.clone())
7957 .map_err(|e| runtime_err(format!("MCP tool error: {e}")))?;
7958 result_str = mcp_result
7959 .content
7960 .iter()
7961 .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
7962 .collect::<Vec<_>>()
7963 .join("\n");
7964 } else {
7965 result_str = self.execute_tool_call(&tc.name, &tc.input)?;
7966 }
7967 }
7968 #[cfg(not(feature = "mcp"))]
7969 {
7970 result_str = self.execute_tool_call(&tc.name, &tc.input)?;
7971 }
7972
7973 let hook_name = format!("__agent_{}_on_tool_call__", agent_def.name);
7975 if let Some(hook) = self.globals.get(&hook_name).cloned() {
7976 let hook_args = vec![
7977 VmValue::String(Arc::from(tc.name.as_str())),
7978 self.json_value_to_vm(&tc.input),
7979 VmValue::String(Arc::from(result_str.as_str())),
7980 ];
7981 let _ = self.call_value(hook, &hook_args);
7982 }
7983
7984 results.push((tc.name.clone(), result_str));
7985 }
7986
7987 let result_msgs = format_tool_result_messages(provider, &tool_calls, &results);
7989 messages.extend(result_msgs);
7990 }
7991 }
7992 }
7993
7994 Err(runtime_err(format!(
7995 "Agent '{}' exceeded max_turns ({})",
7996 agent_def.name, agent_def.max_turns
7997 )))
7998 }
7999
8000 #[cfg(feature = "native")]
8001 fn execute_tool_call(
8002 &mut self,
8003 tool_name: &str,
8004 input: &serde_json::Value,
8005 ) -> Result<String, TlError> {
8006 let func = self
8008 .globals
8009 .get(tool_name)
8010 .ok_or_else(|| runtime_err(format!("Agent tool function '{tool_name}' not found")))?
8011 .clone();
8012
8013 let args = self.json_to_vm_args(input);
8015
8016 let result = self.call_value(func, &args)?;
8018
8019 Ok(format!("{result}"))
8021 }
8022
8023 #[cfg(feature = "native")]
8024 fn json_to_vm_args(&self, input: &serde_json::Value) -> Vec<VmValue> {
8025 match input {
8026 serde_json::Value::Object(map) => {
8027 map.values().map(|v| self.json_value_to_vm(v)).collect()
8029 }
8030 serde_json::Value::Array(arr) => arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8031 _ => vec![self.json_value_to_vm(input)],
8032 }
8033 }
8034
8035 #[cfg(feature = "native")]
8036 fn json_value_to_vm(&self, val: &serde_json::Value) -> VmValue {
8037 match val {
8038 serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
8039 serde_json::Value::Number(n) => {
8040 if let Some(i) = n.as_i64() {
8041 VmValue::Int(i)
8042 } else if let Some(f) = n.as_f64() {
8043 VmValue::Float(f)
8044 } else {
8045 VmValue::None
8046 }
8047 }
8048 serde_json::Value::Bool(b) => VmValue::Bool(*b),
8049 serde_json::Value::Null => VmValue::None,
8050 serde_json::Value::Array(arr) => VmValue::List(Box::new(
8051 arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8052 )),
8053 serde_json::Value::Object(map) => {
8054 let pairs: Vec<(Arc<str>, VmValue)> = map
8055 .iter()
8056 .map(|(k, v)| (Arc::from(k.as_str()), self.json_value_to_vm(v)))
8057 .collect();
8058 VmValue::Map(Box::new(pairs))
8059 }
8060 }
8061 }
8062
8063 #[cfg(feature = "native")]
8064 fn call_value(&mut self, func: VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
8065 match &func {
8066 VmValue::Function(_) => {
8067 let save_len = self.stack.len();
8069 let func_slot = save_len;
8070 let _args_start = func_slot + 1;
8071 self.stack.push(func.clone());
8072 for arg in args {
8073 self.stack.push(arg.clone());
8074 }
8075 self.ensure_stack(self.stack.len() + 256);
8076
8077 self.do_call(func, func_slot, 0, 1, args.len() as u8)?;
8078
8079 let entry_depth = self.frames.len() - 1;
8081 while self.frames.len() > entry_depth {
8082 if self.run_step(entry_depth)?.is_some() {
8083 break;
8084 }
8085 }
8086
8087 let result = self.stack[func_slot].clone();
8089 self.stack.truncate(save_len);
8090 Ok(result)
8091 }
8092 VmValue::Builtin(id) => {
8093 let id_u16 = *id as u16;
8094 let save_len = self.stack.len();
8095 for arg in args {
8096 self.stack.push(arg.clone());
8097 }
8098 let result = self.call_builtin(id_u16, save_len, args.len())?;
8099 self.stack.truncate(save_len);
8100 Ok(result)
8101 }
8102 _ => Err(runtime_err(format!(
8103 "Agent tool '{}' is not callable",
8104 func.type_name()
8105 ))),
8106 }
8107 }
8108
8109 #[cfg(feature = "native")]
8110 fn parse_window_type(s: &str) -> Option<tl_stream::window::WindowType> {
8111 if let Some(dur) = s.strip_prefix("tumbling:") {
8112 let ms = tl_stream::parse_duration(dur).ok()?;
8113 Some(tl_stream::window::WindowType::Tumbling { duration_ms: ms })
8114 } else if let Some(rest) = s.strip_prefix("sliding:") {
8115 let parts: Vec<&str> = rest.splitn(2, ':').collect();
8116 if parts.len() == 2 {
8117 let wms = tl_stream::parse_duration(parts[0]).ok()?;
8118 let sms = tl_stream::parse_duration(parts[1]).ok()?;
8119 Some(tl_stream::window::WindowType::Sliding {
8120 window_ms: wms,
8121 slide_ms: sms,
8122 })
8123 } else {
8124 None
8125 }
8126 } else if let Some(dur) = s.strip_prefix("session:") {
8127 let ms = tl_stream::parse_duration(dur).ok()?;
8128 Some(tl_stream::window::WindowType::Session { gap_ms: ms })
8129 } else {
8130 None
8131 }
8132 }
8133
8134 #[cfg(feature = "native")]
8135 fn handle_connector_decl(
8136 &mut self,
8137 frame_idx: usize,
8138 type_const: u8,
8139 config_const: u8,
8140 ) -> Result<VmValue, TlError> {
8141 let frame = &self.frames[frame_idx];
8142 let connector_type = match &frame.prototype.constants[type_const as usize] {
8143 Constant::String(s) => s.to_string(),
8144 _ => return Err(runtime_err("Expected string constant for connector type")),
8145 };
8146
8147 let config_args = match &frame.prototype.constants[config_const as usize] {
8148 Constant::AstExprList(args) => args.clone(),
8149 _ => return Err(runtime_err("Expected AST expr list for connector config")),
8150 };
8151
8152 let mut properties = std::collections::HashMap::new();
8153 for arg in &config_args {
8154 if let AstExpr::NamedArg { name: key, value } = arg {
8155 let val_str = match value.as_ref() {
8156 AstExpr::String(s) => s.clone(),
8157 AstExpr::Int(n) => n.to_string(),
8158 AstExpr::Float(f) => f.to_string(),
8159 AstExpr::Bool(b) => b.to_string(),
8160 other => {
8161 if let AstExpr::Ident(ident) = other {
8163 if let Some(val) = self.globals.get(ident.as_str()) {
8164 format!("{val}")
8165 } else {
8166 ident.clone()
8167 }
8168 } else {
8169 format!("{other:?}")
8170 }
8171 }
8172 };
8173 properties.insert(key.clone(), val_str);
8174 }
8175 }
8176
8177 let config = tl_stream::ConnectorConfig {
8178 name: String::new(), connector_type,
8180 properties,
8181 };
8182
8183 Ok(VmValue::Connector(Arc::new(config)))
8184 }
8185
8186 fn generator_next(&mut self, gen_arc: &Arc<Mutex<VmGenerator>>) -> Result<VmValue, TlError> {
8188 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8189 if gn.done {
8190 return Ok(VmValue::None);
8191 }
8192 match &mut gn.kind {
8193 GeneratorKind::UserDefined {
8194 prototype,
8195 upvalues,
8196 saved_stack,
8197 ip,
8198 } => {
8199 let proto = prototype.clone();
8200 let uvs = upvalues.clone();
8201 let stack_snapshot = saved_stack.clone();
8202 let saved_ip = *ip;
8203 drop(gn); let new_base = self.stack.len();
8207 let num_regs = proto.num_registers as usize;
8208 self.ensure_stack(new_base + num_regs + 1);
8209 for (i, val) in stack_snapshot.iter().enumerate() {
8211 self.stack[new_base + i] = val.clone();
8212 }
8213
8214 self.frames.push(CallFrame {
8215 prototype: proto,
8216 ip: saved_ip,
8217 base: new_base,
8218 upvalues: uvs,
8219 });
8220
8221 self.yielded_value = None;
8222 let _result = self.run()?;
8223
8224 if let Some(yielded) = self.yielded_value.take() {
8225 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8227 if let GeneratorKind::UserDefined {
8228 saved_stack, ip, ..
8229 } = &mut gn.kind
8230 {
8231 let num_regs_save = saved_stack.len();
8233 for (i, slot) in saved_stack.iter_mut().enumerate().take(num_regs_save) {
8234 if new_base + i < self.stack.len() {
8235 *slot = self.stack[new_base + i].clone();
8236 }
8237 }
8238 *ip = self.yielded_ip;
8240 }
8241 self.stack.truncate(new_base);
8242 Ok(yielded)
8243 } else {
8244 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8246 gn.done = true;
8247 self.stack.truncate(new_base);
8248 Ok(VmValue::None)
8249 }
8250 }
8251 GeneratorKind::ListIter { items, index } => {
8252 if *index < items.len() {
8253 let val = items[*index].clone();
8254 *index += 1;
8255 Ok(val)
8256 } else {
8257 gn.done = true;
8258 Ok(VmValue::None)
8259 }
8260 }
8261 GeneratorKind::Take { source, remaining } => {
8262 if *remaining == 0 {
8263 gn.done = true;
8264 return Ok(VmValue::None);
8265 }
8266 *remaining -= 1;
8267 let src = source.clone();
8268 drop(gn);
8269 let val = self.generator_next(&src)?;
8270 if matches!(val, VmValue::None) {
8271 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8272 gn.done = true;
8273 }
8274 Ok(val)
8275 }
8276 GeneratorKind::Skip { source, remaining } => {
8277 let src = source.clone();
8278 let skip_n = *remaining;
8279 *remaining = 0;
8280 drop(gn);
8281 for _ in 0..skip_n {
8283 let val = self.generator_next(&src)?;
8284 if matches!(val, VmValue::None) {
8285 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8286 gn.done = true;
8287 return Ok(VmValue::None);
8288 }
8289 }
8290 let val = self.generator_next(&src)?;
8291 if matches!(val, VmValue::None) {
8292 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8293 gn.done = true;
8294 }
8295 Ok(val)
8296 }
8297 GeneratorKind::Map { source, func } => {
8298 let src = source.clone();
8299 let f = func.clone();
8300 drop(gn);
8301 let val = self.generator_next(&src)?;
8302 if matches!(val, VmValue::None) {
8303 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8304 gn.done = true;
8305 return Ok(VmValue::None);
8306 }
8307 self.call_vm_function(&f, &[val])
8308 }
8309 GeneratorKind::Filter { source, func } => {
8310 let src = source.clone();
8311 let f = func.clone();
8312 drop(gn);
8313 loop {
8314 let val = self.generator_next(&src)?;
8315 if matches!(val, VmValue::None) {
8316 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8317 gn.done = true;
8318 return Ok(VmValue::None);
8319 }
8320 let test = self.call_vm_function(&f, std::slice::from_ref(&val))?;
8321 if test.is_truthy() {
8322 return Ok(val);
8323 }
8324 }
8325 }
8326 GeneratorKind::Chain {
8327 first,
8328 second,
8329 on_second,
8330 } => {
8331 if !*on_second {
8332 let src = first.clone();
8333 drop(gn);
8334 let val = self.generator_next(&src)?;
8335 if matches!(val, VmValue::None) {
8336 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8337 if let GeneratorKind::Chain {
8338 on_second, second, ..
8339 } = &mut gn.kind
8340 {
8341 *on_second = true;
8342 let src2 = second.clone();
8343 drop(gn);
8344 return self.generator_next(&src2);
8345 }
8346 }
8347 Ok(val)
8348 } else {
8349 let src = second.clone();
8350 drop(gn);
8351 let val = self.generator_next(&src)?;
8352 if matches!(val, VmValue::None) {
8353 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8354 gn.done = true;
8355 }
8356 Ok(val)
8357 }
8358 }
8359 GeneratorKind::Zip { first, second } => {
8360 let src1 = first.clone();
8361 let src2 = second.clone();
8362 drop(gn);
8363 let val1 = self.generator_next(&src1)?;
8364 let val2 = self.generator_next(&src2)?;
8365 if matches!(val1, VmValue::None) || matches!(val2, VmValue::None) {
8366 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8367 gn.done = true;
8368 return Ok(VmValue::None);
8369 }
8370 Ok(VmValue::List(Box::new(vec![val1, val2])))
8371 }
8372 GeneratorKind::Enumerate { source, index } => {
8373 let src = source.clone();
8374 let idx = *index;
8375 *index += 1;
8376 drop(gn);
8377 let val = self.generator_next(&src)?;
8378 if matches!(val, VmValue::None) {
8379 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8380 gn.done = true;
8381 return Ok(VmValue::None);
8382 }
8383 Ok(VmValue::List(Box::new(vec![VmValue::Int(idx as i64), val])))
8384 }
8385 }
8386 }
8387
8388 #[cfg(feature = "native")]
8390 fn process_schema_global(&mut self, s: &str) {
8391 let rest = &s["__schema__:".len()..];
8393 let parts: Vec<&str> = rest.splitn(3, ':').collect();
8394 if parts.len() < 2 {
8395 return;
8396 }
8397
8398 let schema_name = parts[0];
8399 let mut version: i64 = 0;
8400 let fields_str;
8401
8402 if parts.len() == 3 && parts[1].starts_with('v') {
8403 version = parts[1][1..].parse().unwrap_or(0);
8405 fields_str = parts[2];
8406 } else if parts.len() == 3 {
8407 fields_str = &rest[schema_name.len() + 1..];
8409 } else {
8410 fields_str = parts[1];
8411 }
8412
8413 if version == 0 {
8414 return;
8415 } let mut arrow_fields = Vec::new();
8418 for field_pair in fields_str.split(',') {
8419 let kv: Vec<&str> = field_pair.splitn(2, ':').collect();
8420 if kv.len() == 2 {
8421 let fname = kv[0].trim();
8422 let ftype = kv[1].trim();
8423 let type_name = if ftype.starts_with("Simple(\"") && ftype.ends_with("\")") {
8425 &ftype[8..ftype.len() - 2]
8426 } else {
8427 ftype
8428 };
8429 let dt = crate::schema::type_name_to_arrow_pub(type_name);
8430 arrow_fields.push(tl_data::ArrowField::new(fname, dt, true));
8431 }
8432 }
8433
8434 if !arrow_fields.is_empty() {
8435 let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(arrow_fields));
8436 let _ = self.schema_registry.register(
8437 schema_name,
8438 version,
8439 schema,
8440 crate::schema::SchemaMetadata::default(),
8441 );
8442 }
8443 }
8444
8445 #[cfg(feature = "native")]
8447 fn process_migrate_global(&mut self, s: &str) {
8448 let rest = &s["__migrate__:".len()..];
8450 let parts: Vec<&str> = rest.splitn(4, ':').collect();
8451 if parts.len() < 4 {
8452 return;
8453 }
8454
8455 let schema_name = parts[0];
8456 let from_ver: i64 = parts[1].parse().unwrap_or(0);
8457 let to_ver: i64 = parts[2].parse().unwrap_or(0);
8458 let ops_str = parts[3];
8459
8460 let mut ops = Vec::new();
8461 for op_str in ops_str.split(';') {
8462 let op_parts: Vec<&str> = op_str.splitn(4, ':').collect();
8463 if op_parts.is_empty() {
8464 continue;
8465 }
8466 match op_parts[0] {
8467 "add" if op_parts.len() >= 3 => {
8468 let name = op_parts[1].to_string();
8469 let type_raw = op_parts[2];
8471 let type_name =
8472 if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8473 type_raw[8..type_raw.len() - 2].to_string()
8474 } else {
8475 type_raw.to_string()
8476 };
8477 let default = if op_parts.len() >= 4 && op_parts[3].starts_with("default:") {
8478 Some(
8479 op_parts[3]["default:".len()..]
8480 .trim_matches('"')
8481 .to_string(),
8482 )
8483 } else {
8484 None
8485 };
8486 ops.push(crate::schema::MigrationOp::AddColumn {
8487 name,
8488 type_name,
8489 default,
8490 });
8491 }
8492 "drop" if op_parts.len() >= 2 => {
8493 ops.push(crate::schema::MigrationOp::DropColumn {
8494 name: op_parts[1].to_string(),
8495 });
8496 }
8497 "rename" if op_parts.len() >= 3 => {
8498 ops.push(crate::schema::MigrationOp::RenameColumn {
8499 from: op_parts[1].to_string(),
8500 to: op_parts[2].to_string(),
8501 });
8502 }
8503 "alter" if op_parts.len() >= 3 => {
8504 let type_raw = op_parts[2];
8505 let type_name =
8506 if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8507 type_raw[8..type_raw.len() - 2].to_string()
8508 } else {
8509 type_raw.to_string()
8510 };
8511 ops.push(crate::schema::MigrationOp::AlterType {
8512 column: op_parts[1].to_string(),
8513 new_type: type_name,
8514 });
8515 }
8516 _ => {}
8517 }
8518 }
8519
8520 let _ = self
8521 .schema_registry
8522 .apply_migration(schema_name, from_ver, to_ver, &ops);
8523 }
8524
8525 fn deep_clone_value(&self, val: &VmValue) -> Result<VmValue, TlError> {
8528 match val {
8529 VmValue::List(items) => {
8530 let cloned: Result<Vec<_>, _> =
8531 items.iter().map(|v| self.deep_clone_value(v)).collect();
8532 Ok(VmValue::List(Box::new(cloned?)))
8533 }
8534 VmValue::Map(pairs) => {
8535 let cloned: Result<Vec<_>, _> = pairs
8536 .iter()
8537 .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8538 .collect();
8539 Ok(VmValue::Map(Box::new(cloned?)))
8540 }
8541 VmValue::Set(items) => {
8542 let cloned: Result<Vec<_>, _> =
8543 items.iter().map(|v| self.deep_clone_value(v)).collect();
8544 Ok(VmValue::Set(Box::new(cloned?)))
8545 }
8546 VmValue::StructInstance(inst) => {
8547 let cloned_fields: Result<Vec<_>, _> = inst
8548 .fields
8549 .iter()
8550 .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8551 .collect();
8552 Ok(VmValue::StructInstance(Arc::new(VmStructInstance {
8553 type_name: inst.type_name.clone(),
8554 fields: cloned_fields?,
8555 })))
8556 }
8557 VmValue::EnumInstance(e) => {
8558 let cloned_fields: Result<Vec<_>, _> =
8559 e.fields.iter().map(|v| self.deep_clone_value(v)).collect();
8560 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
8561 type_name: e.type_name.clone(),
8562 variant: e.variant.clone(),
8563 fields: cloned_fields?,
8564 })))
8565 }
8566 #[cfg(feature = "gpu")]
8567 VmValue::GpuTensor(gt) => {
8568 let cloned = tl_gpu::GpuTensor::clone(gt.as_ref());
8569 Ok(VmValue::GpuTensor(Arc::new(cloned)))
8570 }
8571 VmValue::Ref(inner) => self.deep_clone_value(inner),
8572 VmValue::Moved => Err(runtime_err("Cannot clone a moved value".to_string())),
8573 VmValue::Task(_) => Err(runtime_err("Cannot clone a task".to_string())),
8574 VmValue::Channel(_) => Err(runtime_err("Cannot clone a channel".to_string())),
8575 VmValue::Generator(_) => Err(runtime_err("Cannot clone a generator".to_string())),
8576 other => Ok(other.clone()),
8577 }
8578 }
8579
8580 pub fn dispatch_method(
8581 &mut self,
8582 obj: VmValue,
8583 method: &str,
8584 args: &[VmValue],
8585 ) -> Result<VmValue, TlError> {
8586 if method == "clone" {
8588 return self.deep_clone_value(&obj);
8589 }
8590 let obj = match obj {
8592 VmValue::Ref(inner) => inner.as_ref().clone(),
8593 other => other,
8594 };
8595 match &obj {
8596 VmValue::String(s) => self.dispatch_string_method(s.clone(), method, args),
8597 VmValue::List(items) => self.dispatch_list_method((**items).clone(), method, args),
8598 VmValue::Map(pairs) => self.dispatch_map_method((**pairs).clone(), method, args),
8599 VmValue::Set(items) => self.dispatch_set_method((**items).clone(), method, args),
8600 VmValue::Module(m) => {
8601 if let Some(func) = m.exports.get(method).cloned() {
8602 self.call_vm_function(&func, args)
8603 } else {
8604 Err(runtime_err(format!(
8605 "Module '{}' has no export '{}'",
8606 m.name, method
8607 )))
8608 }
8609 }
8610 VmValue::StructInstance(inst) => {
8611 let mangled = format!("{}::{}", inst.type_name, method);
8613 if let Some(func) = self.globals.get(&mangled).cloned() {
8614 let mut all_args = vec![obj.clone()];
8615 all_args.extend_from_slice(args);
8616 self.call_vm_function(&func, &all_args)
8617 } else {
8618 Err(runtime_err(format!(
8619 "No method '{}' on struct '{}'",
8620 method, inst.type_name
8621 )))
8622 }
8623 }
8624 #[cfg(feature = "python")]
8625 VmValue::PyObject(wrapper) => crate::python::py_call_method(wrapper, method, args),
8626 #[cfg(feature = "gpu")]
8627 VmValue::GpuTensor(gt) => match method {
8628 "to_cpu" => {
8629 let cpu = gt.to_cpu().map_err(runtime_err)?;
8630 Ok(VmValue::Tensor(Arc::new(cpu)))
8631 }
8632 "shape" => {
8633 let shape_list = gt.shape.iter().map(|&d| VmValue::Int(d as i64)).collect();
8634 Ok(VmValue::List(shape_list))
8635 }
8636 "dtype" => Ok(VmValue::String(Arc::from(format!("{}", gt.dtype).as_str()))),
8637 _ => Err(runtime_err(format!("No method '{}' on gpu_tensor", method))),
8638 },
8639 _ => {
8640 let type_name = obj.type_name();
8642 let mangled = format!("{}::{}", type_name, method);
8643 if let Some(func) = self.globals.get(&mangled).cloned() {
8644 let mut all_args = vec![obj];
8645 all_args.extend_from_slice(args);
8646 self.call_vm_function(&func, &all_args)
8647 } else {
8648 Err(runtime_err(format!(
8649 "No method '{}' on type '{}'",
8650 method, type_name
8651 )))
8652 }
8653 }
8654 }
8655 }
8656
8657 fn dispatch_string_method(
8659 &self,
8660 s: Arc<str>,
8661 method: &str,
8662 args: &[VmValue],
8663 ) -> Result<VmValue, TlError> {
8664 match method {
8665 "len" => Ok(VmValue::Int(s.len() as i64)),
8666 "split" => {
8667 let sep = match args.first() {
8668 Some(VmValue::String(sep)) => sep.to_string(),
8669 _ => return Err(runtime_err("split() expects a string separator")),
8670 };
8671 let parts: Vec<VmValue> = s
8672 .split(&sep)
8673 .map(|p| VmValue::String(Arc::from(p)))
8674 .collect();
8675 Ok(VmValue::List(Box::new(parts)))
8676 }
8677 "trim" => Ok(VmValue::String(Arc::from(s.trim()))),
8678 "contains" => {
8679 let needle = match args.first() {
8680 Some(VmValue::String(n)) => n.to_string(),
8681 _ => return Err(runtime_err("contains() expects a string")),
8682 };
8683 Ok(VmValue::Bool(s.contains(&needle)))
8684 }
8685 "replace" => {
8686 if args.len() < 2 {
8687 return Err(runtime_err("replace() expects 2 arguments (old, new)"));
8688 }
8689 let old = match &args[0] {
8690 VmValue::String(s) => s.to_string(),
8691 _ => return Err(runtime_err("replace() arg must be string")),
8692 };
8693 let new = match &args[1] {
8694 VmValue::String(s) => s.to_string(),
8695 _ => return Err(runtime_err("replace() arg must be string")),
8696 };
8697 Ok(VmValue::String(Arc::from(s.replace(&old, &new).as_str())))
8698 }
8699 "starts_with" => {
8700 let prefix = match args.first() {
8701 Some(VmValue::String(p)) => p.to_string(),
8702 _ => return Err(runtime_err("starts_with() expects a string")),
8703 };
8704 Ok(VmValue::Bool(s.starts_with(&prefix)))
8705 }
8706 "ends_with" => {
8707 let suffix = match args.first() {
8708 Some(VmValue::String(p)) => p.to_string(),
8709 _ => return Err(runtime_err("ends_with() expects a string")),
8710 };
8711 Ok(VmValue::Bool(s.ends_with(&suffix)))
8712 }
8713 "to_upper" => Ok(VmValue::String(Arc::from(s.to_uppercase().as_str()))),
8714 "to_lower" => Ok(VmValue::String(Arc::from(s.to_lowercase().as_str()))),
8715 "chars" => {
8716 let chars: Vec<VmValue> = s
8717 .chars()
8718 .map(|c| VmValue::String(Arc::from(c.to_string().as_str())))
8719 .collect();
8720 Ok(VmValue::List(Box::new(chars)))
8721 }
8722 "repeat" => {
8723 let n = match args.first() {
8724 Some(VmValue::Int(n)) => *n as usize,
8725 _ => return Err(runtime_err("repeat() expects an integer")),
8726 };
8727 Ok(VmValue::String(Arc::from(s.repeat(n).as_str())))
8728 }
8729 "index_of" => {
8730 let needle = match args.first() {
8731 Some(VmValue::String(n)) => n.to_string(),
8732 _ => return Err(runtime_err("index_of() expects a string")),
8733 };
8734 Ok(VmValue::Int(
8735 s.find(&needle).map(|i| i as i64).unwrap_or(-1),
8736 ))
8737 }
8738 "substring" => {
8739 if args.len() < 2 {
8740 return Err(runtime_err("substring() expects start and end"));
8741 }
8742 let start = match &args[0] {
8743 VmValue::Int(n) => *n as usize,
8744 _ => return Err(runtime_err("substring() expects integers")),
8745 };
8746 let end = match &args[1] {
8747 VmValue::Int(n) => *n as usize,
8748 _ => return Err(runtime_err("substring() expects integers")),
8749 };
8750 let end = end.min(s.len());
8751 let start = start.min(end);
8752 Ok(VmValue::String(Arc::from(&s[start..end])))
8753 }
8754 "pad_left" => {
8755 if args.is_empty() {
8756 return Err(runtime_err("pad_left() expects width"));
8757 }
8758 let width = match &args[0] {
8759 VmValue::Int(n) => *n as usize,
8760 _ => return Err(runtime_err("pad_left() expects integer width")),
8761 };
8762 let ch = match args.get(1) {
8763 Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8764 _ => ' ',
8765 };
8766 if s.len() >= width {
8767 Ok(VmValue::String(s))
8768 } else {
8769 Ok(VmValue::String(Arc::from(
8770 format!(
8771 "{}{}",
8772 std::iter::repeat_n(ch, width - s.len()).collect::<String>(),
8773 s
8774 )
8775 .as_str(),
8776 )))
8777 }
8778 }
8779 "pad_right" => {
8780 if args.is_empty() {
8781 return Err(runtime_err("pad_right() expects width"));
8782 }
8783 let width = match &args[0] {
8784 VmValue::Int(n) => *n as usize,
8785 _ => return Err(runtime_err("pad_right() expects integer width")),
8786 };
8787 let ch = match args.get(1) {
8788 Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8789 _ => ' ',
8790 };
8791 if s.len() >= width {
8792 Ok(VmValue::String(s))
8793 } else {
8794 Ok(VmValue::String(Arc::from(
8795 format!(
8796 "{}{}",
8797 s,
8798 std::iter::repeat_n(ch, width - s.len()).collect::<String>()
8799 )
8800 .as_str(),
8801 )))
8802 }
8803 }
8804 "join" => {
8805 let items = match args.first() {
8807 Some(VmValue::List(items)) => items,
8808 _ => return Err(runtime_err("join() expects a list")),
8809 };
8810 let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
8811 Ok(VmValue::String(Arc::from(parts.join(s.as_ref()).as_str())))
8812 }
8813 "trim_start" => Ok(VmValue::String(Arc::from(s.trim_start()))),
8814 "trim_end" => Ok(VmValue::String(Arc::from(s.trim_end()))),
8815 "count" => {
8816 if args.is_empty() {
8817 return Err(runtime_err("count() expects a substring"));
8818 }
8819 if let VmValue::String(sub) = &args[0] {
8820 Ok(VmValue::Int(s.matches(sub.as_ref()).count() as i64))
8821 } else {
8822 Err(runtime_err("count() expects a string"))
8823 }
8824 }
8825 "is_empty" => Ok(VmValue::Bool(s.is_empty())),
8826 "is_numeric" => Ok(VmValue::Bool(
8827 s.chars()
8828 .all(|c| c.is_ascii_digit() || c == '.' || c == '-'),
8829 )),
8830 "is_alpha" => Ok(VmValue::Bool(
8831 !s.is_empty() && s.chars().all(|c| c.is_alphabetic()),
8832 )),
8833 "strip_prefix" => {
8834 if args.is_empty() {
8835 return Err(runtime_err("strip_prefix() expects a string"));
8836 }
8837 if let VmValue::String(prefix) = &args[0] {
8838 match s.strip_prefix(prefix.as_ref()) {
8839 Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8840 None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8841 }
8842 } else {
8843 Err(runtime_err("strip_prefix() expects a string"))
8844 }
8845 }
8846 "strip_suffix" => {
8847 if args.is_empty() {
8848 return Err(runtime_err("strip_suffix() expects a string"));
8849 }
8850 if let VmValue::String(suffix) = &args[0] {
8851 match s.strip_suffix(suffix.as_ref()) {
8852 Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8853 None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8854 }
8855 } else {
8856 Err(runtime_err("strip_suffix() expects a string"))
8857 }
8858 }
8859 _ => Err(runtime_err(format!("No method '{}' on string", method))),
8860 }
8861 }
8862
8863 fn dispatch_list_method(
8865 &mut self,
8866 items: Vec<VmValue>,
8867 method: &str,
8868 args: &[VmValue],
8869 ) -> Result<VmValue, TlError> {
8870 match method {
8871 "len" => Ok(VmValue::Int(items.len() as i64)),
8872 "push" => {
8873 if args.is_empty() {
8874 return Err(runtime_err("push() expects 1 argument"));
8875 }
8876 let mut new_items = items;
8877 new_items.push(args[0].clone());
8878 Ok(VmValue::List(Box::new(new_items)))
8879 }
8880 "map" => {
8881 if args.is_empty() {
8882 return Err(runtime_err("map() expects a function"));
8883 }
8884 let func = &args[0];
8885 let mut result = Vec::new();
8886 for item in items {
8887 let val = self.call_vm_function(func, &[item])?;
8888 result.push(val);
8889 }
8890 Ok(VmValue::List(Box::new(result)))
8891 }
8892 "filter" => {
8893 if args.is_empty() {
8894 return Err(runtime_err("filter() expects a function"));
8895 }
8896 let func = &args[0];
8897 let mut result = Vec::new();
8898 for item in items {
8899 let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
8900 if val.is_truthy() {
8901 result.push(item);
8902 }
8903 }
8904 Ok(VmValue::List(Box::new(result)))
8905 }
8906 "reduce" => {
8907 if args.len() < 2 {
8908 return Err(runtime_err("reduce() expects initial value and function"));
8909 }
8910 let mut acc = args[0].clone();
8911 let func = &args[1];
8912 for item in items {
8913 acc = self.call_vm_function(func, &[acc, item])?;
8914 }
8915 Ok(acc)
8916 }
8917 "sort" => {
8918 let mut sorted = items;
8919 sorted.sort_by(|a, b| match (a, b) {
8920 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
8921 (VmValue::Float(x), VmValue::Float(y)) => {
8922 x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
8923 }
8924 (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
8925 _ => std::cmp::Ordering::Equal,
8926 });
8927 Ok(VmValue::List(Box::new(sorted)))
8928 }
8929 "reverse" => {
8930 let mut reversed = items;
8931 reversed.reverse();
8932 Ok(VmValue::List(Box::new(reversed)))
8933 }
8934 "contains" => {
8935 if args.is_empty() {
8936 return Err(runtime_err("contains() expects a value"));
8937 }
8938 let needle = &args[0];
8939 let found = items.iter().any(|item| match (item, needle) {
8940 (VmValue::Int(a), VmValue::Int(b)) => a == b,
8941 (VmValue::Float(a), VmValue::Float(b)) => a == b,
8942 (VmValue::String(a), VmValue::String(b)) => a == b,
8943 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
8944 (VmValue::None, VmValue::None) => true,
8945 _ => false,
8946 });
8947 Ok(VmValue::Bool(found))
8948 }
8949 "index_of" => {
8950 if args.is_empty() {
8951 return Err(runtime_err("index_of() expects a value"));
8952 }
8953 let needle = &args[0];
8954 let idx = items.iter().position(|item| match (item, needle) {
8955 (VmValue::Int(a), VmValue::Int(b)) => a == b,
8956 (VmValue::Float(a), VmValue::Float(b)) => a == b,
8957 (VmValue::String(a), VmValue::String(b)) => a == b,
8958 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
8959 (VmValue::None, VmValue::None) => true,
8960 _ => false,
8961 });
8962 Ok(VmValue::Int(idx.map(|i| i as i64).unwrap_or(-1)))
8963 }
8964 "slice" => {
8965 if args.len() < 2 {
8966 return Err(runtime_err("slice() expects start and end"));
8967 }
8968 let start = match &args[0] {
8969 VmValue::Int(n) => *n as usize,
8970 _ => return Err(runtime_err("slice() expects integers")),
8971 };
8972 let end = match &args[1] {
8973 VmValue::Int(n) => *n as usize,
8974 _ => return Err(runtime_err("slice() expects integers")),
8975 };
8976 let end = end.min(items.len());
8977 let start = start.min(end);
8978 Ok(VmValue::List(Box::new(items[start..end].to_vec())))
8979 }
8980 "flat_map" => {
8981 if args.is_empty() {
8982 return Err(runtime_err("flat_map() expects a function"));
8983 }
8984 let func = &args[0];
8985 let mut result = Vec::new();
8986 for item in items {
8987 let val = self.call_vm_function(func, &[item])?;
8988 match val {
8989 VmValue::List(sub) => result.extend(*sub),
8990 other => result.push(other),
8991 }
8992 }
8993 Ok(VmValue::List(Box::new(result)))
8994 }
8995 "find" => {
8996 if args.is_empty() {
8997 return Err(runtime_err("find() expects a predicate function"));
8998 }
8999 let func = &args[0];
9000 for item in items {
9001 let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9002 if val.is_truthy() {
9003 return Ok(item);
9004 }
9005 }
9006 Ok(VmValue::None)
9007 }
9008 "sort_by" => {
9009 if args.is_empty() {
9010 return Err(runtime_err("sort_by() expects a key function"));
9011 }
9012 let func = &args[0];
9013 let mut keyed: Vec<(VmValue, VmValue)> = Vec::with_capacity(items.len());
9014 for item in items {
9015 let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9016 keyed.push((key, item));
9017 }
9018 keyed.sort_by(|(a, _), (b, _)| match (a, b) {
9019 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9020 (VmValue::Float(x), VmValue::Float(y)) => {
9021 x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9022 }
9023 (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9024 _ => std::cmp::Ordering::Equal,
9025 });
9026 Ok(VmValue::List(Box::new(
9027 keyed.into_iter().map(|(_, v)| v).collect(),
9028 )))
9029 }
9030 "group_by" => {
9031 if args.is_empty() {
9032 return Err(runtime_err("group_by() expects a key function"));
9033 }
9034 let func = &args[0];
9035 let mut groups: Vec<(Arc<str>, Vec<VmValue>)> = Vec::new();
9036 for item in items {
9037 let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9038 let key_str: Arc<str> = match &key {
9039 VmValue::String(s) => s.clone(),
9040 other => Arc::from(format!("{other}").as_str()),
9041 };
9042 if let Some(group) = groups.iter_mut().find(|(k, _)| *k == key_str) {
9043 group.1.push(item);
9044 } else {
9045 groups.push((key_str, vec![item]));
9046 }
9047 }
9048 let map_pairs: Vec<(Arc<str>, VmValue)> = groups
9049 .into_iter()
9050 .map(|(k, v)| (k, VmValue::List(Box::new(v))))
9051 .collect();
9052 Ok(VmValue::Map(Box::new(map_pairs)))
9053 }
9054 "unique" => {
9055 let mut seen = Vec::new();
9056 let mut result = Vec::new();
9057 for item in &items {
9058 let is_dup = seen.iter().any(|s| vm_values_equal(s, item));
9059 if !is_dup {
9060 seen.push(item.clone());
9061 result.push(item.clone());
9062 }
9063 }
9064 Ok(VmValue::List(Box::new(result)))
9065 }
9066 "flatten" => {
9067 let mut result = Vec::new();
9068 for item in items {
9069 match item {
9070 VmValue::List(sub) => result.extend(*sub),
9071 other => result.push(other),
9072 }
9073 }
9074 Ok(VmValue::List(Box::new(result)))
9075 }
9076 "chunk" => {
9077 if args.is_empty() {
9078 return Err(runtime_err("chunk() expects a size"));
9079 }
9080 let n = match &args[0] {
9081 VmValue::Int(n) if *n > 0 => *n as usize,
9082 _ => return Err(runtime_err("chunk() expects a positive integer")),
9083 };
9084 let chunks: Vec<VmValue> = items
9085 .chunks(n)
9086 .map(|c| VmValue::List(Box::new(c.to_vec())))
9087 .collect();
9088 Ok(VmValue::List(Box::new(chunks)))
9089 }
9090 "insert" => {
9091 if args.len() < 2 {
9092 return Err(runtime_err("insert() expects index and value"));
9093 }
9094 let idx = match &args[0] {
9095 VmValue::Int(n) => *n as usize,
9096 _ => return Err(runtime_err("insert() expects integer index")),
9097 };
9098 let mut new_items = items;
9099 if idx > new_items.len() {
9100 return Err(runtime_err("insert() index out of bounds"));
9101 }
9102 new_items.insert(idx, args[1].clone());
9103 Ok(VmValue::List(Box::new(new_items)))
9104 }
9105 "remove_at" => {
9106 if args.is_empty() {
9107 return Err(runtime_err("remove_at() expects an index"));
9108 }
9109 let idx = match &args[0] {
9110 VmValue::Int(n) => *n as usize,
9111 _ => return Err(runtime_err("remove_at() expects integer index")),
9112 };
9113 let mut new_items = items;
9114 if idx >= new_items.len() {
9115 return Err(runtime_err("remove_at() index out of bounds"));
9116 }
9117 let removed = new_items.remove(idx);
9118 Ok(removed)
9119 }
9120 "is_empty" => Ok(VmValue::Bool(items.is_empty())),
9121 "sum" => {
9122 let mut int_sum: i64 = 0;
9123 let mut has_float = false;
9124 let mut float_sum: f64 = 0.0;
9125 for item in &items {
9126 match item {
9127 VmValue::Int(n) => {
9128 if has_float {
9129 float_sum += *n as f64;
9130 } else {
9131 int_sum += n;
9132 }
9133 }
9134 VmValue::Float(f) => {
9135 if !has_float {
9136 has_float = true;
9137 float_sum = int_sum as f64;
9138 }
9139 float_sum += f;
9140 }
9141 _ => return Err(runtime_err("sum() requires numeric list")),
9142 }
9143 }
9144 if has_float {
9145 Ok(VmValue::Float(float_sum))
9146 } else {
9147 Ok(VmValue::Int(int_sum))
9148 }
9149 }
9150 "min" => {
9151 if items.is_empty() {
9152 return Ok(VmValue::None);
9153 }
9154 let mut min_val = items[0].clone();
9155 for item in &items[1..] {
9156 match (&min_val, item) {
9157 (VmValue::Int(a), VmValue::Int(b)) if b < a => min_val = item.clone(),
9158 (VmValue::Float(a), VmValue::Float(b)) if b < a => min_val = item.clone(),
9159 _ => {}
9160 }
9161 }
9162 Ok(min_val)
9163 }
9164 "max" => {
9165 if items.is_empty() {
9166 return Ok(VmValue::None);
9167 }
9168 let mut max_val = items[0].clone();
9169 for item in &items[1..] {
9170 match (&max_val, item) {
9171 (VmValue::Int(a), VmValue::Int(b)) if b > a => max_val = item.clone(),
9172 (VmValue::Float(a), VmValue::Float(b)) if b > a => max_val = item.clone(),
9173 _ => {}
9174 }
9175 }
9176 Ok(max_val)
9177 }
9178 "each" => {
9179 if args.is_empty() {
9180 return Err(runtime_err("each() expects a function"));
9181 }
9182 let func = &args[0];
9183 for item in items {
9184 self.call_vm_function(func, &[item])?;
9185 }
9186 Ok(VmValue::None)
9187 }
9188 "zip" => {
9189 if args.is_empty() {
9190 return Err(runtime_err("zip() expects a list"));
9191 }
9192 let other = match &args[0] {
9193 VmValue::List(other) => other.as_slice(),
9194 _ => return Err(runtime_err("zip() expects a list")),
9195 };
9196 let len = items.len().min(other.len());
9197 let zipped: Vec<VmValue> = items[..len]
9198 .iter()
9199 .zip(other[..len].iter())
9200 .map(|(a, b)| VmValue::List(Box::new(vec![a.clone(), b.clone()])))
9201 .collect();
9202 Ok(VmValue::List(Box::new(zipped)))
9203 }
9204 "join" => {
9205 let sep = match args.first() {
9206 Some(VmValue::String(s)) => s.as_ref(),
9207 _ => "",
9208 };
9209 let s: String = items
9210 .iter()
9211 .map(|v| format!("{v}"))
9212 .collect::<Vec<_>>()
9213 .join(sep);
9214 Ok(VmValue::String(Arc::from(s.as_str())))
9215 }
9216 _ => Err(runtime_err(format!("No method '{}' on list", method))),
9217 }
9218 }
9219
9220 fn dispatch_map_method(
9222 &mut self,
9223 pairs: Vec<(Arc<str>, VmValue)>,
9224 method: &str,
9225 args: &[VmValue],
9226 ) -> Result<VmValue, TlError> {
9227 match method {
9228 "len" => Ok(VmValue::Int(pairs.len() as i64)),
9229 "keys" => Ok(VmValue::List(Box::new(
9230 pairs
9231 .iter()
9232 .map(|(k, _)| VmValue::String(k.clone()))
9233 .collect(),
9234 ))),
9235 "values" => Ok(VmValue::List(Box::new(
9236 pairs.iter().map(|(_, v)| v.clone()).collect(),
9237 ))),
9238 "contains_key" => {
9239 if args.is_empty() {
9240 return Err(runtime_err("contains_key() expects a key"));
9241 }
9242 if let VmValue::String(key) = &args[0] {
9243 Ok(VmValue::Bool(
9244 pairs.iter().any(|(k, _)| k.as_ref() == key.as_ref()),
9245 ))
9246 } else {
9247 Err(runtime_err("contains_key() expects a string key"))
9248 }
9249 }
9250 "remove" => {
9251 if args.is_empty() {
9252 return Err(runtime_err("remove() expects a key"));
9253 }
9254 if let VmValue::String(key) = &args[0] {
9255 let new_pairs: Vec<(Arc<str>, VmValue)> = pairs
9256 .into_iter()
9257 .filter(|(k, _)| k.as_ref() != key.as_ref())
9258 .collect();
9259 Ok(VmValue::Map(Box::new(new_pairs)))
9260 } else {
9261 Err(runtime_err("remove() expects a string key"))
9262 }
9263 }
9264 "get" => {
9265 if args.is_empty() {
9266 return Err(runtime_err("get() expects a key"));
9267 }
9268 if let VmValue::String(key) = &args[0] {
9269 let default = args.get(1).cloned().unwrap_or(VmValue::None);
9270 let found = pairs.iter().find(|(k, _)| k.as_ref() == key.as_ref());
9271 Ok(found.map(|(_, v)| v.clone()).unwrap_or(default))
9272 } else {
9273 Err(runtime_err("get() expects a string key"))
9274 }
9275 }
9276 "merge" => {
9277 if args.is_empty() {
9278 return Err(runtime_err("merge() expects a map"));
9279 }
9280 if let VmValue::Map(other) = &args[0] {
9281 let mut merged = pairs;
9282 for (k, v) in other.iter() {
9283 if let Some(existing) =
9284 merged.iter_mut().find(|(mk, _)| mk.as_ref() == k.as_ref())
9285 {
9286 existing.1 = v.clone();
9287 } else {
9288 merged.push((k.clone(), v.clone()));
9289 }
9290 }
9291 Ok(VmValue::Map(Box::new(merged)))
9292 } else {
9293 Err(runtime_err("merge() expects a map"))
9294 }
9295 }
9296 "entries" => {
9297 let entries: Vec<VmValue> = pairs
9298 .iter()
9299 .map(|(k, v)| {
9300 VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]))
9301 })
9302 .collect();
9303 Ok(VmValue::List(Box::new(entries)))
9304 }
9305 "map_values" => {
9306 if args.is_empty() {
9307 return Err(runtime_err("map_values() expects a function"));
9308 }
9309 let func = &args[0];
9310 let mut result = Vec::new();
9311 for (k, v) in pairs {
9312 let new_v = self.call_vm_function(func, &[v])?;
9313 result.push((k, new_v));
9314 }
9315 Ok(VmValue::Map(Box::new(result)))
9316 }
9317 "filter" => {
9318 if args.is_empty() {
9319 return Err(runtime_err("filter() expects a predicate function"));
9320 }
9321 let func = &args[0];
9322 let mut result = Vec::new();
9323 for (k, v) in pairs {
9324 let val =
9325 self.call_vm_function(func, &[VmValue::String(k.clone()), v.clone()])?;
9326 if val.is_truthy() {
9327 result.push((k, v));
9328 }
9329 }
9330 Ok(VmValue::Map(Box::new(result)))
9331 }
9332 "set" => {
9333 if args.len() < 2 {
9334 return Err(runtime_err("set() expects key and value"));
9335 }
9336 if let VmValue::String(key) = &args[0] {
9337 let mut new_pairs = pairs;
9338 if let Some(existing) = new_pairs
9339 .iter_mut()
9340 .find(|(k, _)| k.as_ref() == key.as_ref())
9341 {
9342 existing.1 = args[1].clone();
9343 } else {
9344 new_pairs.push((key.clone(), args[1].clone()));
9345 }
9346 Ok(VmValue::Map(Box::new(new_pairs)))
9347 } else {
9348 Err(runtime_err("set() expects a string key"))
9349 }
9350 }
9351 "is_empty" => Ok(VmValue::Bool(pairs.is_empty())),
9352 _ => Err(runtime_err(format!("No method '{}' on map", method))),
9353 }
9354 }
9355
9356 fn dispatch_set_method(
9358 &self,
9359 items: Vec<VmValue>,
9360 method: &str,
9361 args: &[VmValue],
9362 ) -> Result<VmValue, TlError> {
9363 match method {
9364 "len" => Ok(VmValue::Int(items.len() as i64)),
9365 "contains" => {
9366 if args.is_empty() {
9367 return Err(runtime_err("contains() expects a value"));
9368 }
9369 Ok(VmValue::Bool(
9370 items.iter().any(|x| vm_values_equal(x, &args[0])),
9371 ))
9372 }
9373 "add" => {
9374 if args.is_empty() {
9375 return Err(runtime_err("add() expects a value"));
9376 }
9377 let mut new_items = items;
9378 if !new_items.iter().any(|x| vm_values_equal(x, &args[0])) {
9379 new_items.push(args[0].clone());
9380 }
9381 Ok(VmValue::Set(Box::new(new_items)))
9382 }
9383 "remove" => {
9384 if args.is_empty() {
9385 return Err(runtime_err("remove() expects a value"));
9386 }
9387 let new_items: Vec<VmValue> = items
9388 .into_iter()
9389 .filter(|x| !vm_values_equal(x, &args[0]))
9390 .collect();
9391 Ok(VmValue::Set(Box::new(new_items)))
9392 }
9393 "to_list" => Ok(VmValue::List(Box::new(items))),
9394 "union" => {
9395 if args.is_empty() {
9396 return Err(runtime_err("union() expects a set"));
9397 }
9398 if let VmValue::Set(b) = &args[0] {
9399 let mut result = items;
9400 for item in b.iter() {
9401 if !result.iter().any(|x| vm_values_equal(x, item)) {
9402 result.push(item.clone());
9403 }
9404 }
9405 Ok(VmValue::Set(Box::new(result)))
9406 } else {
9407 Err(runtime_err("union() expects a set"))
9408 }
9409 }
9410 "intersection" => {
9411 if args.is_empty() {
9412 return Err(runtime_err("intersection() expects a set"));
9413 }
9414 if let VmValue::Set(b) = &args[0] {
9415 let result: Vec<VmValue> = items
9416 .into_iter()
9417 .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
9418 .collect();
9419 Ok(VmValue::Set(Box::new(result)))
9420 } else {
9421 Err(runtime_err("intersection() expects a set"))
9422 }
9423 }
9424 "difference" => {
9425 if args.is_empty() {
9426 return Err(runtime_err("difference() expects a set"));
9427 }
9428 if let VmValue::Set(b) = &args[0] {
9429 let result: Vec<VmValue> = items
9430 .into_iter()
9431 .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
9432 .collect();
9433 Ok(VmValue::Set(Box::new(result)))
9434 } else {
9435 Err(runtime_err("difference() expects a set"))
9436 }
9437 }
9438 _ => Err(runtime_err(format!("No method '{}' on set", method))),
9439 }
9440 }
9441
9442 #[cfg(feature = "native")]
9444 fn handle_import(&mut self, path: &str, alias: &str) -> Result<VmValue, TlError> {
9445 let resolved = if let Some(ref base) = self.file_path {
9447 let base_dir = std::path::Path::new(base)
9448 .parent()
9449 .unwrap_or(std::path::Path::new("."));
9450 let candidate = base_dir.join(path);
9451 if candidate.exists() {
9452 candidate.to_string_lossy().to_string()
9453 } else {
9454 path.to_string()
9455 }
9456 } else {
9457 path.to_string()
9458 };
9459
9460 if self.importing_files.contains(&resolved) {
9462 return Err(runtime_err(format!("Circular import detected: {resolved}")));
9463 }
9464
9465 if let Some(exports) = self.module_cache.get(&resolved) {
9467 let exports = exports.clone();
9468 return self.bind_import_exports(exports, alias);
9469 }
9470
9471 let source = std::fs::read_to_string(&resolved)
9473 .map_err(|e| runtime_err(format!("Cannot import '{}': {}", resolved, e)))?;
9474 let program = tl_parser::parse(&source)
9475 .map_err(|e| runtime_err(format!("Parse error in '{}': {}", resolved, e)))?;
9476 let proto = crate::compiler::compile(&program)
9477 .map_err(|e| runtime_err(format!("Compile error in '{}': {}", resolved, e)))?;
9478
9479 self.importing_files.insert(resolved.clone());
9481
9482 let mut import_vm = Vm::new();
9484 import_vm.file_path = Some(resolved.clone());
9485 import_vm.globals = self.globals.clone();
9486 import_vm.importing_files = self.importing_files.clone();
9487 import_vm.module_cache = self.module_cache.clone();
9488 import_vm.package_roots = self.package_roots.clone();
9489 import_vm.project_root = self.project_root.clone();
9490 import_vm.execute(&proto)?;
9491
9492 self.importing_files.remove(&resolved);
9493
9494 let mut exports = HashMap::new();
9496
9497 for (k, v) in &import_vm.globals {
9499 if !self.globals.contains_key(k) {
9500 exports.insert(k.clone(), v.clone());
9501 }
9502 }
9503
9504 for (name, reg) in &proto.top_level_locals {
9506 if !name.starts_with("__enum_") && !exports.contains_key(name) {
9507 let stack_idx = reg;
9508 if (*stack_idx as usize) < import_vm.stack.len() {
9509 let val = import_vm.stack[*stack_idx as usize].clone();
9510 if !matches!(val, VmValue::None) || name.starts_with("_") {
9511 exports.insert(name.clone(), val);
9512 }
9513 }
9514 }
9515 }
9516
9517 self.module_cache.insert(resolved, exports.clone());
9519 for (k, v) in import_vm.module_cache {
9521 self.module_cache.entry(k).or_insert(v);
9522 }
9523
9524 self.bind_import_exports(exports, alias)
9525 }
9526
9527 #[cfg(feature = "native")]
9529 fn bind_import_exports(
9530 &mut self,
9531 exports: HashMap<String, VmValue>,
9532 alias: &str,
9533 ) -> Result<VmValue, TlError> {
9534 if alias.is_empty() {
9535 for (k, v) in &exports {
9537 self.globals.insert(k.clone(), v.clone());
9538 }
9539 Ok(VmValue::None)
9540 } else {
9541 let module = VmModule {
9543 name: Arc::from(alias),
9544 exports,
9545 };
9546 let module_val = VmValue::Module(Arc::new(module));
9547 self.globals.insert(alias.to_string(), module_val.clone());
9548 Ok(module_val)
9549 }
9550 }
9551
9552 #[cfg(feature = "native")]
9554 fn handle_use_import(
9555 &mut self,
9556 path_str: &str,
9557 extra_a: u8,
9558 kind: u8,
9559 _frame_idx: usize,
9560 ) -> Result<VmValue, TlError> {
9561 match kind {
9562 0 => {
9563 let segments: Vec<&str> = path_str.split('.').collect();
9565 let file_path = self.resolve_use_path(&segments)?;
9566 let _last = segments.last().copied().unwrap_or("");
9568 self.handle_import(&file_path, "")?;
9569 Ok(VmValue::None)
9574 }
9575 1 => {
9576 let brace_start = path_str.find('{').unwrap_or(path_str.len());
9578 let prefix = path_str[..brace_start].trim_end_matches('.');
9579 let segments: Vec<&str> = prefix.split('.').collect();
9580 let file_path = self.resolve_use_path(&segments)?;
9581 self.handle_import(&file_path, "")?;
9582 Ok(VmValue::None)
9583 }
9584 2 => {
9585 let prefix = path_str.trim_end_matches(".*");
9587 let segments: Vec<&str> = prefix.split('.').collect();
9588 let file_path = self.resolve_use_path(&segments)?;
9589 self.handle_import(&file_path, "")?;
9590 Ok(VmValue::None)
9591 }
9592 3 => {
9593 let segments: Vec<&str> = path_str.split('.').collect();
9595 let file_path = self.resolve_use_path(&segments)?;
9596 let alias_str = if let Some(frame) = self.frames.last() {
9599 if let Some(crate::chunk::Constant::String(s)) =
9600 frame.prototype.constants.get(extra_a as usize)
9601 {
9602 s.to_string()
9603 } else {
9604 segments.last().copied().unwrap_or("module").to_string()
9605 }
9606 } else {
9607 segments.last().copied().unwrap_or("module").to_string()
9608 };
9609 self.handle_import(&file_path, &alias_str)?;
9610 Ok(VmValue::None)
9611 }
9612 _ => Err(runtime_err(format!("Unknown use-import kind: {kind}"))),
9613 }
9614 }
9615
9616 #[cfg(feature = "native")]
9618 fn resolve_use_path(&self, segments: &[&str]) -> Result<String, TlError> {
9619 if segments.contains(&"..") {
9621 return Err(runtime_err("Import paths cannot contain '..'"));
9622 }
9623
9624 let base_dir = if let Some(ref fp) = self.file_path {
9625 std::path::Path::new(fp)
9626 .parent()
9627 .unwrap_or(std::path::Path::new("."))
9628 .to_path_buf()
9629 } else {
9630 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
9631 };
9632
9633 let rel_path = segments.join("/");
9634
9635 let file_path = base_dir.join(format!("{rel_path}.tl"));
9637 if file_path.exists() {
9638 return Ok(file_path.to_string_lossy().to_string());
9639 }
9640
9641 let dir_path = base_dir.join(&rel_path).join("mod.tl");
9643 if dir_path.exists() {
9644 return Ok(dir_path.to_string_lossy().to_string());
9645 }
9646
9647 if segments.len() > 1 {
9649 let parent = &segments[..segments.len() - 1];
9650 let parent_path = parent.join("/");
9651 let parent_file = base_dir.join(format!("{parent_path}.tl"));
9652 if parent_file.exists() {
9653 return Ok(parent_file.to_string_lossy().to_string());
9654 }
9655 let parent_dir = base_dir.join(&parent_path).join("mod.tl");
9656 if parent_dir.exists() {
9657 return Ok(parent_dir.to_string_lossy().to_string());
9658 }
9659 }
9660
9661 let pkg_name_underscore = segments[0];
9664 let pkg_name_hyphen = pkg_name_underscore.replace('_', "-");
9665 let pkg_root = self
9666 .package_roots
9667 .get(pkg_name_underscore)
9668 .or_else(|| self.package_roots.get(&pkg_name_hyphen));
9669
9670 if let Some(root) = pkg_root {
9671 let remaining = &segments[1..];
9672 if let Some(path) = resolve_package_file(root, remaining) {
9673 return Ok(path);
9674 }
9675 }
9676
9677 Err(runtime_err(format!(
9678 "Module not found: `{}`",
9679 segments.join(".")
9680 )))
9681 }
9682
9683 fn call_vm_function(&mut self, func: &VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
9685 match func {
9686 VmValue::Function(closure) => {
9687 let proto = closure.prototype.clone();
9688 let arity = proto.arity as usize;
9689 if args.len() != arity {
9690 return Err(runtime_err(format!(
9691 "Expected {} arguments, got {}",
9692 arity,
9693 args.len()
9694 )));
9695 }
9696
9697 if proto.is_generator {
9699 let mut closed_upvalues = Vec::new();
9700 for uv in &closure.upvalues {
9701 match uv {
9702 UpvalueRef::Open { stack_index } => {
9703 let val = self.stack[*stack_index].clone();
9704 closed_upvalues.push(UpvalueRef::Closed(val));
9705 }
9706 UpvalueRef::Closed(v) => {
9707 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
9708 }
9709 }
9710 }
9711 let num_regs = proto.num_registers as usize;
9712 let mut saved_stack = vec![VmValue::None; num_regs];
9713 for (i, arg) in args.iter().enumerate() {
9714 saved_stack[i] = arg.clone();
9715 }
9716 let gn = VmGenerator::new(GeneratorKind::UserDefined {
9717 prototype: proto,
9718 upvalues: closed_upvalues,
9719 saved_stack,
9720 ip: 0,
9721 });
9722 return Ok(VmValue::Generator(Arc::new(Mutex::new(gn))));
9723 }
9724
9725 let new_base = self.stack.len();
9726 self.ensure_stack(new_base + proto.num_registers as usize + 1);
9727
9728 for (i, arg) in args.iter().enumerate() {
9729 self.stack[new_base + i] = arg.clone();
9730 }
9731
9732 self.frames.push(CallFrame {
9733 prototype: proto,
9734 ip: 0,
9735 base: new_base,
9736 upvalues: closure.upvalues.clone(),
9737 });
9738
9739 let result = self.run()?;
9740 self.stack.truncate(new_base);
9741 Ok(result)
9742 }
9743 VmValue::Builtin(id) => {
9744 let args_base = self.stack.len();
9746 for arg in args {
9747 self.stack.push(arg.clone());
9748 }
9749 let result = self.call_builtin(*id as u16, args_base, args.len());
9750 self.stack.truncate(args_base);
9751 result
9752 }
9753 _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
9754 }
9755 }
9756
9757 #[cfg(feature = "native")]
9760 fn handle_table_pipe(
9761 &mut self,
9762 frame_idx: usize,
9763 table_val: VmValue,
9764 op_const: u8,
9765 args_const: u8,
9766 ) -> Result<VmValue, TlError> {
9767 let df = match table_val {
9768 VmValue::Table(t) => t.df,
9769 other => {
9770 return self.table_pipe_fallback(other, frame_idx, op_const, args_const);
9772 }
9773 };
9774
9775 let frame = &self.frames[frame_idx];
9776 let op_name = match &frame.prototype.constants[op_const as usize] {
9777 Constant::String(s) => s.to_string(),
9778 _ => return Err(runtime_err("Expected string constant for table op")),
9779 };
9780 let ast_args = match &frame.prototype.constants[args_const as usize] {
9781 Constant::AstExprList(args) => args.clone(),
9782 _ => return Err(runtime_err("Expected AST expr list for table args")),
9783 };
9784
9785 let ctx = self.build_translate_context();
9786
9787 match op_name.as_str() {
9788 "filter" => {
9789 if ast_args.len() != 1 {
9790 return Err(runtime_err("filter() expects 1 argument (predicate)"));
9791 }
9792 let pred = translate_expr(&ast_args[0], &ctx).map_err(runtime_err)?;
9793 let filtered = df.filter(pred).map_err(|e| runtime_err(format!("{e}")))?;
9794 Ok(VmValue::Table(VmTable { df: filtered }))
9795 }
9796 "select" => {
9797 if ast_args.is_empty() {
9798 return Err(runtime_err("select() expects at least 1 argument"));
9799 }
9800 let mut select_exprs = Vec::new();
9801 for arg in &ast_args {
9802 match arg {
9803 AstExpr::Ident(name) => select_exprs.push(col(name.as_str())),
9804 AstExpr::NamedArg { name, value } => {
9805 let expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9806 select_exprs.push(expr.alias(name));
9807 }
9808 AstExpr::String(name) => select_exprs.push(col(name.as_str())),
9809 other => {
9810 let expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9811 select_exprs.push(expr);
9812 }
9813 }
9814 }
9815 let selected = df
9816 .select(select_exprs)
9817 .map_err(|e| runtime_err(format!("{e}")))?;
9818 Ok(VmValue::Table(VmTable { df: selected }))
9819 }
9820 "sort" => {
9821 if ast_args.is_empty() {
9822 return Err(runtime_err("sort() expects at least 1 argument (column)"));
9823 }
9824 let mut sort_exprs = Vec::new();
9825 let mut i = 0;
9826 while i < ast_args.len() {
9827 let col_name = match &ast_args[i] {
9828 AstExpr::Ident(name) => name.clone(),
9829 AstExpr::String(name) => name.clone(),
9830 _ => {
9831 return Err(runtime_err(
9832 "sort() column must be an identifier or string",
9833 ));
9834 }
9835 };
9836 i += 1;
9837 let ascending = if i < ast_args.len() {
9838 match &ast_args[i] {
9839 AstExpr::String(dir) if dir == "desc" || dir == "DESC" => {
9840 i += 1;
9841 false
9842 }
9843 AstExpr::String(dir) if dir == "asc" || dir == "ASC" => {
9844 i += 1;
9845 true
9846 }
9847 _ => true,
9848 }
9849 } else {
9850 true
9851 };
9852 sort_exprs.push(col(col_name.as_str()).sort(ascending, true));
9853 }
9854 let sorted = df
9855 .sort(sort_exprs)
9856 .map_err(|e| runtime_err(format!("{e}")))?;
9857 Ok(VmValue::Table(VmTable { df: sorted }))
9858 }
9859 "with" => {
9860 if ast_args.len() != 1 {
9861 return Err(runtime_err(
9862 "with() expects 1 argument (map of column definitions)",
9863 ));
9864 }
9865 let pairs = match &ast_args[0] {
9866 AstExpr::Map(pairs) => pairs,
9867 _ => return Err(runtime_err("with() expects a map { col = expr, ... }")),
9868 };
9869 let mut result_df = df;
9870 for (key, value_expr) in pairs {
9871 let col_name = match key {
9872 AstExpr::String(s) => s.clone(),
9873 AstExpr::Ident(s) => s.clone(),
9874 _ => return Err(runtime_err("with() key must be a string or identifier")),
9875 };
9876 let df_expr = translate_expr(value_expr, &ctx).map_err(runtime_err)?;
9877 result_df = result_df
9878 .with_column(&col_name, df_expr)
9879 .map_err(|e| runtime_err(format!("{e}")))?;
9880 }
9881 Ok(VmValue::Table(VmTable { df: result_df }))
9882 }
9883 "aggregate" => {
9884 let mut group_by_cols: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9885 let mut agg_exprs: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9886 for arg in &ast_args {
9887 match arg {
9888 AstExpr::NamedArg { name, value } if name == "by" => match value.as_ref() {
9889 AstExpr::String(col_name) => group_by_cols.push(col(col_name.as_str())),
9890 AstExpr::Ident(col_name) => group_by_cols.push(col(col_name.as_str())),
9891 AstExpr::List(items) => {
9892 for item in items {
9893 match item {
9894 AstExpr::String(s) => group_by_cols.push(col(s.as_str())),
9895 AstExpr::Ident(s) => group_by_cols.push(col(s.as_str())),
9896 _ => {
9897 return Err(runtime_err(
9898 "by: list items must be strings or identifiers",
9899 ));
9900 }
9901 }
9902 }
9903 }
9904 _ => return Err(runtime_err("by: must be a column name or list")),
9905 },
9906 AstExpr::NamedArg { name, value } => {
9907 let agg_expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9908 agg_exprs.push(agg_expr.alias(name));
9909 }
9910 other => {
9911 let agg_expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9912 agg_exprs.push(agg_expr);
9913 }
9914 }
9915 }
9916 let aggregated = df
9917 .aggregate(group_by_cols, agg_exprs)
9918 .map_err(|e| runtime_err(format!("{e}")))?;
9919 Ok(VmValue::Table(VmTable { df: aggregated }))
9920 }
9921 "join" => {
9922 if ast_args.is_empty() {
9923 return Err(runtime_err(
9924 "join() expects at least 1 argument (right table)",
9925 ));
9926 }
9927 let right_table = self.eval_ast_to_vm(&ast_args[0])?;
9929 let right_df = match right_table {
9930 VmValue::Table(t) => t.df,
9931 _ => return Err(runtime_err("join() first arg must be a table")),
9932 };
9933 let mut left_cols: Vec<String> = Vec::new();
9934 let mut right_cols: Vec<String> = Vec::new();
9935 let mut join_type = JoinType::Inner;
9936 for arg in &ast_args[1..] {
9937 match arg {
9938 AstExpr::NamedArg { name, value } if name == "on" => {
9939 if let AstExpr::BinOp {
9940 left,
9941 op: tl_ast::BinOp::Eq,
9942 right,
9943 } = value.as_ref()
9944 {
9945 let lc = match left.as_ref() {
9946 AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
9947 _ => {
9948 return Err(runtime_err(
9949 "on: left side must be a column name",
9950 ));
9951 }
9952 };
9953 let rc = match right.as_ref() {
9954 AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
9955 _ => {
9956 return Err(runtime_err(
9957 "on: right side must be a column name",
9958 ));
9959 }
9960 };
9961 left_cols.push(lc);
9962 right_cols.push(rc);
9963 }
9964 }
9965 AstExpr::NamedArg { name, value } if name == "kind" => {
9966 if let AstExpr::String(kind_str) = value.as_ref() {
9967 join_type = match kind_str.as_str() {
9968 "inner" => JoinType::Inner,
9969 "left" => JoinType::Left,
9970 "right" => JoinType::Right,
9971 "full" => JoinType::Full,
9972 _ => {
9973 return Err(runtime_err(format!(
9974 "Unknown join type: {kind_str}"
9975 )));
9976 }
9977 };
9978 }
9979 }
9980 _ => {}
9981 }
9982 }
9983 let lc_refs: Vec<&str> = left_cols.iter().map(|s| s.as_str()).collect();
9984 let rc_refs: Vec<&str> = right_cols.iter().map(|s| s.as_str()).collect();
9985 let joined = df
9986 .join(right_df, join_type, &lc_refs, &rc_refs, None)
9987 .map_err(|e| runtime_err(format!("{e}")))?;
9988 Ok(VmValue::Table(VmTable { df: joined }))
9989 }
9990 "head" | "limit" => {
9991 let n = match ast_args.first() {
9992 Some(AstExpr::Int(n)) => *n as usize,
9993 None => 10,
9994 _ => return Err(runtime_err("head/limit expects an integer")),
9995 };
9996 let limited = df
9997 .limit(0, Some(n))
9998 .map_err(|e| runtime_err(format!("{e}")))?;
9999 Ok(VmValue::Table(VmTable { df: limited }))
10000 }
10001 "collect" => {
10002 let batches = self.engine().collect(df).map_err(runtime_err)?;
10003 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10004 Ok(VmValue::String(Arc::from(formatted.as_str())))
10005 }
10006 "show" => {
10007 let limit = match ast_args.first() {
10008 Some(AstExpr::Int(n)) => *n as usize,
10009 None => 20,
10010 _ => 20,
10011 };
10012 let limited = df
10013 .limit(0, Some(limit))
10014 .map_err(|e| runtime_err(format!("{e}")))?;
10015 let batches = self.engine().collect(limited).map_err(runtime_err)?;
10016 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10017 println!("{formatted}");
10018 self.output.push(formatted);
10019 Ok(VmValue::None)
10020 }
10021 "describe" => {
10022 let schema = df.schema();
10023 let mut lines = Vec::new();
10024 lines.push("Columns:".to_string());
10025 for field in schema.fields() {
10026 lines.push(format!(" {}: {}", field.name(), field.data_type()));
10027 }
10028 let output = lines.join("\n");
10029 println!("{output}");
10030 self.output.push(output.clone());
10031 Ok(VmValue::String(Arc::from(output.as_str())))
10032 }
10033 "write_csv" => {
10034 if ast_args.len() != 1 {
10035 return Err(runtime_err("write_csv() expects 1 argument (path)"));
10036 }
10037 let path = self.eval_ast_to_string(&ast_args[0])?;
10038 self.engine().write_csv(df, &path).map_err(runtime_err)?;
10039 Ok(VmValue::None)
10040 }
10041 "write_parquet" => {
10042 if ast_args.len() != 1 {
10043 return Err(runtime_err("write_parquet() expects 1 argument (path)"));
10044 }
10045 let path = self.eval_ast_to_string(&ast_args[0])?;
10046 self.engine()
10047 .write_parquet(df, &path)
10048 .map_err(runtime_err)?;
10049 Ok(VmValue::None)
10050 }
10051 "fill_null" => {
10053 if ast_args.is_empty() {
10054 return Err(runtime_err(
10055 "fill_null() expects (column, [strategy/value])",
10056 ));
10057 }
10058 let column = self.eval_ast_to_string(&ast_args[0])?;
10059 if ast_args.len() >= 2 {
10060 let val = self.eval_ast_to_vm(&ast_args[1])?;
10061 match val {
10062 VmValue::String(s) => {
10063 let fill_val = if ast_args.len() >= 3 {
10065 match self.eval_ast_to_vm(&ast_args[2])? {
10066 VmValue::Int(n) => Some(n as f64),
10067 VmValue::Float(f) => Some(f),
10068 _ => None,
10069 }
10070 } else {
10071 None
10072 };
10073 let result = self
10074 .engine()
10075 .fill_null(df, &column, &s, fill_val)
10076 .map_err(runtime_err)?;
10077 Ok(VmValue::Table(VmTable { df: result }))
10078 }
10079 VmValue::Int(n) => {
10080 let result = self
10081 .engine()
10082 .fill_null(df, &column, "value", Some(n as f64))
10083 .map_err(runtime_err)?;
10084 Ok(VmValue::Table(VmTable { df: result }))
10085 }
10086 VmValue::Float(f) => {
10087 let result = self
10088 .engine()
10089 .fill_null(df, &column, "value", Some(f))
10090 .map_err(runtime_err)?;
10091 Ok(VmValue::Table(VmTable { df: result }))
10092 }
10093 _ => Err(runtime_err(
10094 "fill_null() second arg must be a strategy or fill value",
10095 )),
10096 }
10097 } else {
10098 let result = self
10099 .engine()
10100 .fill_null(df, &column, "zero", None)
10101 .map_err(runtime_err)?;
10102 Ok(VmValue::Table(VmTable { df: result }))
10103 }
10104 }
10105 "drop_null" => {
10106 if ast_args.is_empty() {
10107 return Err(runtime_err("drop_null() expects (column)"));
10108 }
10109 let column = self.eval_ast_to_string(&ast_args[0])?;
10110 let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
10111 Ok(VmValue::Table(VmTable { df: result }))
10112 }
10113 "dedup" => {
10114 let columns: Vec<String> = ast_args
10115 .iter()
10116 .filter_map(|a| self.eval_ast_to_string(a).ok())
10117 .collect();
10118 let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
10119 Ok(VmValue::Table(VmTable { df: result }))
10120 }
10121 "clamp" => {
10122 if ast_args.len() < 3 {
10123 return Err(runtime_err("clamp() expects (column, min, max)"));
10124 }
10125 let column = self.eval_ast_to_string(&ast_args[0])?;
10126 let min_val = match self.eval_ast_to_vm(&ast_args[1])? {
10127 VmValue::Int(n) => n as f64,
10128 VmValue::Float(f) => f,
10129 _ => return Err(runtime_err("clamp() min must be a number")),
10130 };
10131 let max_val = match self.eval_ast_to_vm(&ast_args[2])? {
10132 VmValue::Int(n) => n as f64,
10133 VmValue::Float(f) => f,
10134 _ => return Err(runtime_err("clamp() max must be a number")),
10135 };
10136 let result = self
10137 .engine()
10138 .clamp(df, &column, min_val, max_val)
10139 .map_err(runtime_err)?;
10140 Ok(VmValue::Table(VmTable { df: result }))
10141 }
10142 "data_profile" => {
10143 let result = self.engine().data_profile(df).map_err(runtime_err)?;
10144 Ok(VmValue::Table(VmTable { df: result }))
10145 }
10146 "row_count" => {
10147 let count = self.engine().row_count(df).map_err(runtime_err)?;
10148 Ok(VmValue::Int(count))
10149 }
10150 "null_rate" => {
10151 if ast_args.is_empty() {
10152 return Err(runtime_err("null_rate() expects (column)"));
10153 }
10154 let column = self.eval_ast_to_string(&ast_args[0])?;
10155 let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
10156 Ok(VmValue::Float(rate))
10157 }
10158 "is_unique" => {
10159 if ast_args.is_empty() {
10160 return Err(runtime_err("is_unique() expects (column)"));
10161 }
10162 let column = self.eval_ast_to_string(&ast_args[0])?;
10163 let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
10164 Ok(VmValue::Bool(unique))
10165 }
10166 "window" => {
10168 use tl_data::datafusion::logical_expr::{
10169 WindowFrame, WindowFunctionDefinition,
10170 expr::{Sort as DfSort, WindowFunction as WinFunc},
10171 };
10172 if ast_args.is_empty() {
10173 return Err(runtime_err(
10174 "window() expects named arguments: fn, partition_by, order_by, alias",
10175 ));
10176 }
10177 let mut win_fn_name = String::new();
10178 let mut partition_by_cols: Vec<String> = Vec::new();
10179 let mut order_by_cols: Vec<String> = Vec::new();
10180 let mut alias_name = String::new();
10181 let mut win_args: Vec<String> = Vec::new();
10182 let mut descending = false;
10183
10184 for arg in &ast_args {
10185 if let AstExpr::NamedArg { name, value } = arg {
10186 match name.as_str() {
10187 "fn" => win_fn_name = self.eval_ast_to_string(value)?,
10188 "partition_by" => match value.as_ref() {
10189 AstExpr::List(items) => {
10190 for item in items {
10191 partition_by_cols.push(self.eval_ast_to_string(item)?);
10192 }
10193 }
10194 _ => partition_by_cols.push(self.eval_ast_to_string(value)?),
10195 },
10196 "order_by" => match value.as_ref() {
10197 AstExpr::List(items) => {
10198 for item in items {
10199 order_by_cols.push(self.eval_ast_to_string(item)?);
10200 }
10201 }
10202 _ => order_by_cols.push(self.eval_ast_to_string(value)?),
10203 },
10204 "alias" | "as" => alias_name = self.eval_ast_to_string(value)?,
10205 "args" => match value.as_ref() {
10206 AstExpr::List(items) => {
10207 for item in items {
10208 win_args.push(self.eval_ast_to_string(item)?);
10209 }
10210 }
10211 _ => win_args.push(self.eval_ast_to_string(value)?),
10212 },
10213 "desc" => {
10214 if let AstExpr::Bool(b) = value.as_ref() {
10215 descending = *b;
10216 }
10217 }
10218 _ => {}
10219 }
10220 }
10221 }
10222
10223 if win_fn_name.is_empty() {
10224 return Err(runtime_err(
10225 "window() requires fn: parameter (rank, row_number, dense_rank, lag, lead, ntile)",
10226 ));
10227 }
10228 if alias_name.is_empty() {
10229 alias_name = win_fn_name.clone();
10230 }
10231
10232 let session = self.engine().session_ctx();
10234 let win_udf = match win_fn_name.as_str() {
10235 "rank" => session.udwf("rank"),
10236 "dense_rank" => session.udwf("dense_rank"),
10237 "row_number" => session.udwf("row_number"),
10238 "percent_rank" => session.udwf("percent_rank"),
10239 "cume_dist" => session.udwf("cume_dist"),
10240 "ntile" => session.udwf("ntile"),
10241 "lag" => session.udwf("lag"),
10242 "lead" => session.udwf("lead"),
10243 "first_value" => session.udwf("first_value"),
10244 "last_value" => session.udwf("last_value"),
10245 _ => {
10246 return Err(runtime_err(format!(
10247 "Unknown window function: {win_fn_name}"
10248 )));
10249 }
10250 }
10251 .map_err(|e| {
10252 runtime_err(format!(
10253 "Window function '{win_fn_name}' not available: {e}"
10254 ))
10255 })?;
10256
10257 let fun = WindowFunctionDefinition::WindowUDF(win_udf);
10258
10259 let func_args: Vec<tl_data::datafusion::prelude::Expr> = win_args
10261 .iter()
10262 .map(|a| {
10263 if let Ok(n) = a.parse::<i64>() {
10264 lit(n)
10265 } else {
10266 col(a.as_str())
10267 }
10268 })
10269 .collect();
10270
10271 let partition_exprs: Vec<tl_data::datafusion::prelude::Expr> =
10272 partition_by_cols.iter().map(|c| col(c.as_str())).collect();
10273 let order_exprs: Vec<DfSort> = order_by_cols
10274 .iter()
10275 .map(|c| DfSort::new(col(c.as_str()), !descending, true))
10276 .collect();
10277
10278 let has_order = !order_exprs.is_empty();
10279 let win_expr = tl_data::datafusion::prelude::Expr::WindowFunction(WinFunc {
10280 fun,
10281 args: func_args,
10282 partition_by: partition_exprs,
10283 order_by: order_exprs,
10284 window_frame: WindowFrame::new(if has_order { Some(true) } else { None }),
10285 null_treatment: None,
10286 })
10287 .alias(&alias_name);
10288
10289 let schema = df.schema();
10291 let mut select_exprs: Vec<tl_data::datafusion::prelude::Expr> = schema
10292 .fields()
10293 .iter()
10294 .map(|f| col(f.name().as_str()))
10295 .collect();
10296 select_exprs.push(win_expr);
10297
10298 let result_df = df
10299 .select(select_exprs)
10300 .map_err(|e| runtime_err(format!("Window function error: {e}")))?;
10301 Ok(VmValue::Table(VmTable { df: result_df }))
10302 }
10303 "union" => {
10305 if ast_args.is_empty() {
10306 return Err(runtime_err("union() expects a table argument"));
10307 }
10308 let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10309 let right_df = match right_table {
10310 VmValue::Table(t) => t.df,
10311 _ => return Err(runtime_err("union() argument must be a table")),
10312 };
10313 let result_df = df
10314 .union(right_df)
10315 .map_err(|e| runtime_err(format!("Union error: {e}")))?;
10316 Ok(VmValue::Table(VmTable { df: result_df }))
10317 }
10318 "sample" => {
10320 use tl_data::datafusion::arrow::{array::UInt32Array, compute};
10321 use tl_data::datafusion::datasource::MemTable;
10322 if ast_args.is_empty() {
10323 return Err(runtime_err("sample() expects a count or fraction"));
10324 }
10325 let batches = self.engine().collect(df).map_err(runtime_err)?;
10326 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10327 let sample_count = match &ast_args[0] {
10328 AstExpr::Int(n) => (*n as usize).min(total_rows),
10329 AstExpr::Float(f) if *f > 0.0 && *f <= 1.0 => {
10330 ((total_rows as f64) * f).ceil() as usize
10331 }
10332 _ => {
10333 let val = self.eval_ast_to_string(&ast_args[0])?;
10334 val.parse::<usize>().map_err(|_| {
10335 runtime_err("sample() expects integer count or float fraction")
10336 })?
10337 }
10338 };
10339 if total_rows == 0 || sample_count == 0 {
10340 let schema = batches[0].schema();
10341 let empty = tl_data::datafusion::arrow::record_batch::RecordBatch::new_empty(
10342 schema.clone(),
10343 );
10344 let mem_table = MemTable::try_new(schema, vec![vec![empty]])
10345 .map_err(|e| runtime_err(format!("{e}")))?;
10346 let new_df = self
10347 .engine()
10348 .session_ctx()
10349 .read_table(Arc::new(mem_table))
10350 .map_err(|e| runtime_err(format!("{e}")))?;
10351 return Ok(VmValue::Table(VmTable { df: new_df }));
10352 }
10353 let mut rng = rand::thread_rng();
10355 let mut indices: Vec<usize> = (0..total_rows).collect();
10356 use rand::seq::SliceRandom;
10357 indices.partial_shuffle(&mut rng, sample_count);
10358 indices.truncate(sample_count);
10359 indices.sort();
10360 let combined = compute::concat_batches(&batches[0].schema(), &batches)
10362 .map_err(|e| runtime_err(format!("{e}")))?;
10363 let idx_array =
10364 UInt32Array::from(indices.iter().map(|&i| i as u32).collect::<Vec<_>>());
10365 let sampled_cols: Vec<tl_data::datafusion::arrow::array::ArrayRef> = (0..combined
10366 .num_columns())
10367 .map(|c| {
10368 compute::take(combined.column(c), &idx_array, None)
10369 .map_err(|e| runtime_err(format!("{e}")))
10370 })
10371 .collect::<Result<Vec<_>, _>>()?;
10372 let sampled_batch = tl_data::datafusion::arrow::record_batch::RecordBatch::try_new(
10373 combined.schema(),
10374 sampled_cols,
10375 )
10376 .map_err(|e| runtime_err(format!("{e}")))?;
10377 let mem_table =
10378 MemTable::try_new(sampled_batch.schema(), vec![vec![sampled_batch]])
10379 .map_err(|e| runtime_err(format!("{e}")))?;
10380 let new_df = self
10381 .engine()
10382 .session_ctx()
10383 .read_table(Arc::new(mem_table))
10384 .map_err(|e| runtime_err(format!("{e}")))?;
10385 Ok(VmValue::Table(VmTable { df: new_df }))
10386 }
10387 _ => Err(runtime_err(format!("Unknown table operation: {op_name}"))),
10388 }
10389 }
10390
10391 fn table_pipe_fallback(
10394 &mut self,
10395 left_val: VmValue,
10396 frame_idx: usize,
10397 op_const: u8,
10398 args_const: u8,
10399 ) -> Result<VmValue, TlError> {
10400 let frame = &self.frames[frame_idx];
10401 let op_name = match &frame.prototype.constants[op_const as usize] {
10402 Constant::String(s) => s.to_string(),
10403 _ => return Err(runtime_err("Expected string constant for table op")),
10404 };
10405 let ast_args = match &frame.prototype.constants[args_const as usize] {
10406 Constant::AstExprList(args) => args.clone(),
10407 _ => return Err(runtime_err("Expected AST expr list for table args")),
10408 };
10409
10410 if let Some(builtin_id) = BuiltinId::from_name(&op_name) {
10412 let mut all_args = vec![left_val];
10414 for arg in &ast_args {
10415 all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10416 }
10417 let args_base = self.stack.len();
10418 for arg in &all_args {
10419 self.stack.push(arg.clone());
10420 }
10421 let result = self.call_builtin(builtin_id as u16, args_base, all_args.len());
10422 self.stack.truncate(args_base);
10423 return result;
10424 }
10425
10426 if let Some(func) = self.globals.get(&op_name).cloned() {
10428 let mut all_args = vec![left_val];
10429 for arg in &ast_args {
10430 all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10431 }
10432 return self.call_vm_function(&func, &all_args);
10433 }
10434
10435 Err(runtime_err(format!("Unknown operation: `{op_name}`")))
10436 }
10437
10438 #[cfg(feature = "native")]
10440 fn build_translate_context(&self) -> TranslateContext {
10441 let mut ctx = TranslateContext::new();
10442 for (name, val) in &self.globals {
10444 let local = match val {
10445 VmValue::Int(n) => Some(LocalValue::Int(*n)),
10446 VmValue::Float(f) => Some(LocalValue::Float(*f)),
10447 VmValue::String(s) => Some(LocalValue::String(s.to_string())),
10448 VmValue::Bool(b) => Some(LocalValue::Bool(*b)),
10449 _ => None,
10450 };
10451 if let Some(l) = local {
10452 ctx.locals.insert(name.clone(), l);
10453 }
10454 }
10455 if let Some(frame) = self.frames.last() {
10457 for local_idx in 0..frame.prototype.num_locals as usize {
10458 if let Some(val) = self.stack.get(frame.base + local_idx) {
10459 let _ = val;
10461 }
10462 }
10463 }
10464 ctx
10465 }
10466
10467 fn eval_ast_to_vm(&mut self, expr: &AstExpr) -> Result<VmValue, TlError> {
10470 match expr {
10471 AstExpr::Ident(name) => {
10472 if let Some(val) = self.globals.get(name) {
10474 return Ok(val.clone());
10475 }
10476 if let Some(frame) = self.frames.last() {
10478 for i in 0..frame.prototype.num_registers as usize {
10479 if let Some(val) = self.stack.get(frame.base + i)
10480 && !matches!(val, VmValue::None)
10481 {
10482 }
10485 }
10486 }
10487 Err(runtime_err(format!("Undefined variable: `{name}`")))
10488 }
10489 AstExpr::String(s) => Ok(VmValue::String(Arc::from(s.as_str()))),
10490 AstExpr::Int(n) => Ok(VmValue::Int(*n)),
10491 AstExpr::Float(f) => Ok(VmValue::Float(*f)),
10492 AstExpr::Bool(b) => Ok(VmValue::Bool(*b)),
10493 AstExpr::None => Ok(VmValue::None),
10494 AstExpr::Closure {
10495 params: _, body: _, ..
10496 } => {
10497 use crate::compiler;
10498 let wrapper = tl_ast::Program {
10499 statements: vec![tl_ast::Stmt {
10500 kind: tl_ast::StmtKind::Expr(expr.clone()),
10501 span: tl_errors::Span::new(0, 0),
10502 doc_comment: None,
10503 }],
10504 module_doc: None,
10505 };
10506 let proto = compiler::compile(&wrapper)?;
10507 let mut temp_vm = Vm::new();
10508 temp_vm.globals = self.globals.clone();
10510 let result = temp_vm.execute(&proto)?;
10511 Ok(result)
10512 }
10513 _ => {
10514 let wrapper = tl_ast::Program {
10516 statements: vec![tl_ast::Stmt {
10517 kind: tl_ast::StmtKind::Expr(expr.clone()),
10518 span: tl_errors::Span::new(0, 0),
10519 doc_comment: None,
10520 }],
10521 module_doc: None,
10522 };
10523 use crate::compiler;
10524 let proto = compiler::compile(&wrapper)?;
10525 let mut temp_vm = Vm::new();
10526 temp_vm.globals = self.globals.clone();
10527 temp_vm.execute(&proto)
10528 }
10529 }
10530 }
10531
10532 fn eval_ast_to_string(&mut self, expr: &AstExpr) -> Result<String, TlError> {
10533 match self.eval_ast_to_vm(expr)? {
10534 VmValue::String(s) => Ok(s.to_string()),
10535 _ => Err(runtime_err("Expected a string")),
10536 }
10537 }
10538
10539 fn interpolate_string(&self, s: &str, _base: usize) -> Result<String, TlError> {
10541 let mut result = String::new();
10542 let mut chars = s.chars().peekable();
10543 while let Some(ch) = chars.next() {
10544 if ch == '{' {
10545 let mut var_name = String::new();
10546 let mut depth = 1;
10547 for c in chars.by_ref() {
10548 if c == '{' {
10549 depth += 1;
10550 } else if c == '}' {
10551 depth -= 1;
10552 if depth == 0 {
10553 break;
10554 }
10555 }
10556 var_name.push(c);
10557 }
10558 if let Some(val) = self.globals.get(&var_name) {
10560 result.push_str(&format!("{val}"));
10561 } else {
10562 result.push('{');
10566 result.push_str(&var_name);
10567 result.push('}');
10568 }
10569 } else if ch == '\\' {
10570 match chars.next() {
10571 Some('n') => result.push('\n'),
10572 Some('t') => result.push('\t'),
10573 Some('\\') => result.push('\\'),
10574 Some('"') => result.push('"'),
10575 Some(c) => {
10576 result.push('\\');
10577 result.push(c);
10578 }
10579 None => result.push('\\'),
10580 }
10581 } else {
10582 result.push(ch);
10583 }
10584 }
10585 Ok(result)
10586 }
10587
10588 pub fn execute_single_instruction(
10591 &mut self,
10592 inst: u32,
10593 proto: &Prototype,
10594 base: usize,
10595 ) -> Result<Option<VmValue>, TlError> {
10596 use crate::opcode::{decode_a, decode_b, decode_bx, decode_c, decode_op};
10597
10598 let proto = Arc::new(proto.clone());
10599 self.frames.push(CallFrame {
10601 prototype: proto.clone(),
10602 ip: 0,
10603 base,
10604 upvalues: Vec::new(),
10605 });
10606 let frame_idx = self.frames.len() - 1;
10607
10608 let op = decode_op(inst);
10609 let a = decode_a(inst);
10610 let _b = decode_b(inst);
10611 let _c = decode_c(inst);
10612 let bx = decode_bx(inst);
10613
10614 let result = match op {
10617 Op::GetGlobal => {
10618 let name = self.get_string_constant(frame_idx, bx)?;
10619 let val = self
10620 .globals
10621 .get(name.as_ref())
10622 .cloned()
10623 .unwrap_or(VmValue::None);
10624 self.stack[base + a as usize] = val;
10625 Ok(None)
10626 }
10627 Op::SetGlobal => {
10628 let name = self.get_string_constant(frame_idx, bx)?;
10629 let val = self.stack[base + a as usize].clone();
10630 self.globals.insert(name.to_string(), val);
10631 Ok(None)
10632 }
10633 _ => {
10634 Ok(None)
10637 }
10638 };
10639
10640 self.frames.pop();
10641 result
10642 }
10643}
10644
10645impl Default for Vm {
10646 fn default() -> Self {
10647 Self::new()
10648 }
10649}
10650
10651#[cfg(test)]
10652mod tests {
10653 use super::*;
10654 use crate::compiler::compile;
10655 use tl_parser::parse;
10656
10657 fn run(source: &str) -> Result<VmValue, TlError> {
10658 let program = parse(source)?;
10659 let proto = compile(&program)?;
10660 let mut vm = Vm::new();
10661 vm.execute(&proto)
10662 }
10663
10664 fn run_output(source: &str) -> Vec<String> {
10665 let program = parse(source).unwrap();
10666 let proto = compile(&program).unwrap();
10667 let mut vm = Vm::new();
10668 vm.execute(&proto).unwrap();
10669 vm.output
10670 }
10671
10672 #[test]
10673 fn test_vm_arithmetic() {
10674 assert!(matches!(run("1 + 2").unwrap(), VmValue::Int(3)));
10675 assert!(matches!(run("10 - 3").unwrap(), VmValue::Int(7)));
10676 assert!(matches!(run("4 * 5").unwrap(), VmValue::Int(20)));
10677 assert!(matches!(run("10 / 3").unwrap(), VmValue::Int(3)));
10678 assert!(matches!(run("10 % 3").unwrap(), VmValue::Int(1)));
10679 assert!(matches!(run("2 ** 10").unwrap(), VmValue::Int(1024)));
10680 let output = run_output("print(1 + 2)");
10681 assert_eq!(output, vec!["3"]);
10682 }
10683
10684 #[test]
10685 fn test_vm_let_and_print() {
10686 let output = run_output("let x = 42\nprint(x)");
10687 assert_eq!(output, vec!["42"]);
10688 }
10689
10690 #[test]
10691 fn test_vm_function() {
10692 let output = run_output("fn double(n) { n * 2 }\nlet result = double(21)\nprint(result)");
10693 assert_eq!(output, vec!["42"]);
10694 }
10695
10696 #[test]
10697 fn test_vm_if_else() {
10698 let output =
10699 run_output("let x = 10\nif x > 5 { print(\"big\") } else { print(\"small\") }");
10700 assert_eq!(output, vec!["big"]);
10701 }
10702
10703 #[test]
10704 fn test_vm_list() {
10705 let output = run_output("let items = [1, 2, 3]\nprint(len(items))");
10706 assert_eq!(output, vec!["3"]);
10707 }
10708
10709 #[test]
10710 fn test_vm_map_builtin() {
10711 let output = run_output(
10712 "let nums = [1, 2, 3]\nlet doubled = map(nums, (x) => x * 2)\nprint(doubled)",
10713 );
10714 assert_eq!(output, vec!["[2, 4, 6]"]);
10715 }
10716
10717 #[test]
10718 fn test_vm_filter_builtin() {
10719 let output = run_output(
10720 "let nums = [1, 2, 3, 4, 5]\nlet evens = filter(nums, (x) => x % 2 == 0)\nprint(evens)",
10721 );
10722 assert_eq!(output, vec!["[2, 4]"]);
10723 }
10724
10725 #[test]
10726 fn test_vm_for_loop() {
10727 let output = run_output("let sum = 0\nfor i in range(5) { sum = sum + i }\nprint(sum)");
10728 assert_eq!(output, vec!["10"]);
10729 }
10730
10731 #[test]
10732 fn test_vm_closure() {
10733 let output = run_output("let double = (x) => x * 2\nprint(double(5))");
10734 assert_eq!(output, vec!["10"]);
10735 }
10736
10737 #[test]
10738 fn test_vm_sum() {
10739 let output = run_output("print(sum([1, 2, 3, 4]))");
10740 assert_eq!(output, vec!["10"]);
10741 }
10742
10743 #[test]
10744 fn test_vm_reduce() {
10745 let output = run_output(
10746 "let product = reduce([1, 2, 3, 4], 1, (acc, x) => acc * x)\nprint(product)",
10747 );
10748 assert_eq!(output, vec!["24"]);
10749 }
10750
10751 #[test]
10752 fn test_vm_pipe() {
10753 let output = run_output("let result = [1, 2, 3] |> map((x) => x + 10)\nprint(result)");
10754 assert_eq!(output, vec!["[11, 12, 13]"]);
10755 }
10756
10757 #[test]
10758 fn test_vm_comparison() {
10759 let output = run_output("print(5 > 3)");
10760 assert_eq!(output, vec!["true"]);
10761 }
10762
10763 #[test]
10764 fn test_vm_precedence() {
10765 let output = run_output("print(2 + 3 * 4)");
10766 assert_eq!(output, vec!["14"]);
10767 }
10768
10769 #[test]
10770 fn test_vm_match() {
10771 let output =
10772 run_output("let x = 2\nprint(match x { 1 => \"one\", 2 => \"two\", _ => \"other\" })");
10773 assert_eq!(output, vec!["two"]);
10774 }
10775
10776 #[test]
10777 fn test_vm_match_wildcard() {
10778 let output = run_output("print(match 99 { 1 => \"one\", _ => \"other\" })");
10779 assert_eq!(output, vec!["other"]);
10780 }
10781
10782 #[test]
10783 fn test_vm_match_binding() {
10784 let output = run_output("print(match 42 { val => val + 1 })");
10785 assert_eq!(output, vec!["43"]);
10786 }
10787
10788 #[test]
10789 fn test_vm_match_guard() {
10790 let output = run_output(
10791 "let x = 5\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10792 );
10793 assert_eq!(output, vec!["pos"]);
10794 }
10795
10796 #[test]
10797 fn test_vm_match_guard_negative() {
10798 let output = run_output(
10799 "let x = -3\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10800 );
10801 assert_eq!(output, vec!["neg"]);
10802 }
10803
10804 #[test]
10805 fn test_vm_match_guard_zero() {
10806 let output = run_output(
10807 "let x = 0\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10808 );
10809 assert_eq!(output, vec!["zero"]);
10810 }
10811
10812 #[test]
10813 fn test_vm_match_enum_destructure() {
10814 let output = run_output(
10815 r#"
10816enum Shape { Circle(int64), Rect(int64, int64) }
10817let s = Shape::Circle(5)
10818print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10819"#,
10820 );
10821 assert_eq!(output, vec!["5"]);
10822 }
10823
10824 #[test]
10825 fn test_vm_match_enum_destructure_rect() {
10826 let output = run_output(
10827 r#"
10828enum Shape { Circle(int64), Rect(int64, int64) }
10829let s = Shape::Rect(3, 4)
10830print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10831"#,
10832 );
10833 assert_eq!(output, vec!["12"]);
10834 }
10835
10836 #[test]
10837 fn test_vm_match_enum_wildcard_field() {
10838 let output = run_output(
10839 r#"
10840enum Pair { Two(int64, int64) }
10841let p = Pair::Two(10, 20)
10842print(match p { Pair::Two(_, y) => y, _ => 0 })
10843"#,
10844 );
10845 assert_eq!(output, vec!["20"]);
10846 }
10847
10848 #[test]
10849 fn test_vm_match_enum_guard() {
10850 let output = run_output(
10851 r#"
10852enum Result { Ok(int64), Err(string) }
10853let r = Result::Ok(150)
10854print(match r { Result::Ok(v) if v > 100 => "big", Result::Ok(v) => "small", Result::Err(e) => e, _ => "unknown" })
10855"#,
10856 );
10857 assert_eq!(output, vec!["big"]);
10858 }
10859
10860 #[test]
10861 fn test_vm_match_or_pattern() {
10862 let output =
10863 run_output("let x = 2\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10864 assert_eq!(output, vec!["small"]);
10865 }
10866
10867 #[test]
10868 fn test_vm_match_or_pattern_no_match() {
10869 let output =
10870 run_output("let x = 10\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10871 assert_eq!(output, vec!["big"]);
10872 }
10873
10874 #[test]
10875 fn test_vm_match_string() {
10876 let output = run_output(
10877 r#"let s = "hello"
10878print(match s { "hi" => 1, "hello" => 2, _ => 0 })"#,
10879 );
10880 assert_eq!(output, vec!["2"]);
10881 }
10882
10883 #[test]
10884 fn test_vm_match_bool() {
10885 let output = run_output("print(match true { true => \"yes\", false => \"no\" })");
10886 assert_eq!(output, vec!["yes"]);
10887 }
10888
10889 #[test]
10890 fn test_vm_match_none() {
10891 let output = run_output("print(match none { none => \"nothing\", _ => \"something\" })");
10892 assert_eq!(output, vec!["nothing"]);
10893 }
10894
10895 #[test]
10896 fn test_vm_let_destructure_list() {
10897 let output = run_output("let [a, b, c] = [1, 2, 3]\nprint(a)\nprint(b)\nprint(c)");
10898 assert_eq!(output, vec!["1", "2", "3"]);
10899 }
10900
10901 #[test]
10902 fn test_vm_let_destructure_list_rest() {
10903 let output =
10904 run_output("let [head, ...tail] = [1, 2, 3, 4]\nprint(head)\nprint(len(tail))");
10905 assert_eq!(output, vec!["1", "3"]);
10906 }
10907
10908 #[test]
10909 fn test_vm_let_destructure_struct() {
10910 let output = run_output(
10911 r#"
10912struct Point { x: int64, y: int64 }
10913let p = Point { x: 10, y: 20 }
10914let Point { x, y } = p
10915print(x)
10916print(y)
10917"#,
10918 );
10919 assert_eq!(output, vec!["10", "20"]);
10920 }
10921
10922 #[test]
10923 fn test_vm_let_destructure_struct_anon() {
10924 let output = run_output(
10925 r#"
10926struct Point { x: int64, y: int64 }
10927let p = Point { x: 10, y: 20 }
10928let { x, y } = p
10929print(x)
10930print(y)
10931"#,
10932 );
10933 assert_eq!(output, vec!["10", "20"]);
10934 }
10935
10936 #[test]
10937 fn test_vm_match_struct_pattern() {
10938 let output = run_output(
10939 r#"
10940struct Point { x: int64, y: int64 }
10941let p = Point { x: 1, y: 2 }
10942print(match p { Point { x, y } => x + y, _ => 0 })
10943"#,
10944 );
10945 assert_eq!(output, vec!["3"]);
10946 }
10947
10948 #[test]
10949 fn test_vm_match_list_pattern() {
10950 let output = run_output(
10951 r#"
10952let lst = [1, 2, 3]
10953print(match lst { [a, b, c] => a + b + c, _ => 0 })
10954"#,
10955 );
10956 assert_eq!(output, vec!["6"]);
10957 }
10958
10959 #[test]
10960 fn test_vm_match_list_rest_pattern() {
10961 let output = run_output(
10962 r#"
10963let lst = [10, 20, 30, 40]
10964print(match lst { [head, ...rest] => head, _ => 0 })
10965"#,
10966 );
10967 assert_eq!(output, vec!["10"]);
10968 }
10969
10970 #[test]
10971 fn test_vm_match_list_empty() {
10972 let output = run_output(
10973 r#"
10974let lst = []
10975print(match lst { [] => "empty", _ => "nonempty" })
10976"#,
10977 );
10978 assert_eq!(output, vec!["empty"]);
10979 }
10980
10981 #[test]
10982 fn test_vm_match_list_length_mismatch() {
10983 let output = run_output(
10984 r#"
10985let lst = [1, 2, 3]
10986print(match lst { [a, b] => "two", [a, b, c] => "three", _ => "other" })
10987"#,
10988 );
10989 assert_eq!(output, vec!["three"]);
10990 }
10991
10992 #[test]
10993 fn test_vm_match_negative_literal() {
10994 let output =
10995 run_output("print(match -1 { -1 => \"neg one\", 0 => \"zero\", _ => \"other\" })");
10996 assert_eq!(output, vec!["neg one"]);
10997 }
10998
10999 #[test]
11000 fn test_vm_case_with_pattern() {
11001 let output = run_output(
11002 r#"
11003let x = 5
11004let result = case {
11005 x > 10 => "big",
11006 x > 0 => "positive",
11007 _ => "other"
11008}
11009print(result)
11010"#,
11011 );
11012 assert_eq!(output, vec!["positive"]);
11013 }
11014
11015 #[test]
11016 fn test_vm_parallel_map() {
11017 let result = run("map(range(15000), (x) => x * 2)").unwrap();
11019 if let VmValue::List(items) = result {
11020 assert_eq!(items.len(), 15000);
11021 assert!(matches!(items[0], VmValue::Int(0)));
11022 assert!(matches!(items[1], VmValue::Int(2)));
11023 assert!(matches!(items[14999], VmValue::Int(29998)));
11024 } else {
11025 panic!("Expected list, got {:?}", result);
11026 }
11027 }
11028
11029 #[test]
11030 fn test_vm_parallel_filter() {
11031 let result = run("filter(range(20000), (x) => x % 2 == 0)").unwrap();
11032 if let VmValue::List(items) = result {
11033 assert_eq!(items.len(), 10000);
11034 assert!(matches!(items[0], VmValue::Int(0)));
11035 assert!(matches!(items[1], VmValue::Int(2)));
11036 } else {
11037 panic!("Expected list, got {:?}", result);
11038 }
11039 }
11040
11041 #[test]
11042 fn test_vm_parallel_sum() {
11043 let result = run("sum(range(20000))").unwrap();
11044 assert!(matches!(result, VmValue::Int(199990000)));
11046 }
11047
11048 #[test]
11049 fn test_vm_recursive_fib() {
11050 let output = run_output(
11051 "fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\nprint(fib(10))",
11052 );
11053 assert_eq!(output, vec!["55"]);
11054 }
11055
11056 #[test]
11057 fn test_vm_if_else_expr() {
11058 let output = run_output(
11060 "fn abs(n) { if n < 0 { 0 - n } else { n } }\nprint(abs(-5))\nprint(abs(3))",
11061 );
11062 assert_eq!(output, vec!["5", "3"]);
11063 }
11064
11065 #[test]
11068 fn test_vm_struct_creation() {
11069 let output = run_output(
11070 "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.0, y: 2.0 }\nprint(p.x)\nprint(p.y)",
11071 );
11072 assert_eq!(output, vec!["1.0", "2.0"]);
11073 }
11074
11075 #[test]
11076 fn test_vm_struct_nested() {
11077 let output = run_output(
11078 "struct Point { x: float64, y: float64 }\nstruct Line { start: Point, end_pt: Point }\nlet l = Line { start: Point { x: 0.0, y: 0.0 }, end_pt: Point { x: 1.0, y: 1.0 } }\nprint(l.start.x)",
11079 );
11080 assert_eq!(output, vec!["0.0"]);
11081 }
11082
11083 #[test]
11084 fn test_vm_enum_creation() {
11085 let output = run_output("enum Color { Red, Green, Blue }\nlet c = Color::Red\nprint(c)");
11086 assert_eq!(output, vec!["Color::Red"]);
11087 }
11088
11089 #[test]
11090 fn test_vm_enum_with_fields() {
11091 let output = run_output(
11092 "enum Shape { Circle(float64), Rect(float64, float64) }\nlet s = Shape::Circle(5.0)\nprint(s)",
11093 );
11094 assert!(output[0].contains("Circle"));
11095 }
11096
11097 #[test]
11098 fn test_vm_impl_method() {
11099 let output = run_output(
11100 "struct Counter { value: int64 }\nimpl Counter {\n fn get(self) { self.value }\n}\nlet c = Counter { value: 42 }\nprint(c.get())",
11101 );
11102 assert_eq!(output, vec!["42"]);
11103 }
11104
11105 #[test]
11106 fn test_vm_try_catch_throw() {
11107 let output = run_output("try {\n throw \"oops\"\n} catch e {\n print(e)\n}");
11108 assert_eq!(output, vec!["oops"]);
11109 }
11110
11111 #[test]
11112 fn test_vm_string_split() {
11113 let output = run_output("let parts = \"hello world\".split(\" \")\nprint(parts)");
11114 assert_eq!(output, vec!["[hello, world]"]);
11115 }
11116
11117 #[test]
11118 fn test_vm_string_trim() {
11119 let output = run_output("print(\" hello \".trim())");
11120 assert_eq!(output, vec!["hello"]);
11121 }
11122
11123 #[test]
11124 fn test_vm_string_contains() {
11125 let output = run_output("print(\"hello world\".contains(\"world\"))");
11126 assert_eq!(output, vec!["true"]);
11127 }
11128
11129 #[test]
11130 fn test_vm_string_upper_lower() {
11131 let output = run_output("print(\"hello\".to_upper())\nprint(\"HELLO\".to_lower())");
11132 assert_eq!(output, vec!["HELLO", "hello"]);
11133 }
11134
11135 #[test]
11136 fn test_vm_math_sqrt() {
11137 let output = run_output("print(sqrt(16.0))");
11138 assert_eq!(output, vec!["4.0"]);
11139 }
11140
11141 #[test]
11142 fn test_vm_math_floor_ceil() {
11143 let output = run_output("print(floor(3.7))\nprint(ceil(3.2))");
11144 assert_eq!(output, vec!["3.0", "4.0"]);
11145 }
11146
11147 #[test]
11148 fn test_vm_math_trig() {
11149 let output = run_output("print(sin(0.0))\nprint(cos(0.0))");
11150 assert_eq!(output, vec!["0.0", "1.0"]);
11151 }
11152
11153 #[test]
11154 fn test_vm_assert_pass() {
11155 run("assert(true)").unwrap();
11156 run("assert_eq(1 + 1, 2)").unwrap();
11157 }
11158
11159 #[test]
11160 fn test_vm_assert_fail() {
11161 assert!(run("assert(false)").is_err());
11162 assert!(run("assert_eq(1, 2)").is_err());
11163 }
11164
11165 #[test]
11166 fn test_vm_join() {
11167 let output = run_output("print(join(\", \", [\"a\", \"b\", \"c\"]))");
11168 assert_eq!(output, vec!["a, b, c"]);
11169 }
11170
11171 #[test]
11172 fn test_vm_list_method_len() {
11173 let output = run_output("print([1, 2, 3].len())");
11174 assert_eq!(output, vec!["3"]);
11175 }
11176
11177 #[test]
11178 fn test_vm_list_method_map() {
11179 let output = run_output("print([1, 2, 3].map((x) => x * 2))");
11180 assert_eq!(output, vec!["[2, 4, 6]"]);
11181 }
11182
11183 #[test]
11184 fn test_vm_list_method_filter() {
11185 let output = run_output("print([1, 2, 3, 4, 5].filter((x) => x > 3))");
11186 assert_eq!(output, vec!["[4, 5]"]);
11187 }
11188
11189 #[test]
11190 fn test_vm_string_replace() {
11191 let output = run_output("print(\"hello world\".replace(\"world\", \"rust\"))");
11192 assert_eq!(output, vec!["hello rust"]);
11193 }
11194
11195 #[test]
11196 fn test_vm_string_starts_ends() {
11197 let output = run_output(
11198 "print(\"hello\".starts_with(\"hel\"))\nprint(\"hello\".ends_with(\"llo\"))",
11199 );
11200 assert_eq!(output, vec!["true", "true"]);
11201 }
11202
11203 #[test]
11204 fn test_vm_math_log() {
11205 let result = run("log(1.0)").unwrap();
11206 if let VmValue::Float(f) = result {
11207 assert!((f - 0.0).abs() < 1e-10);
11208 } else {
11209 panic!("Expected float");
11210 }
11211 }
11212
11213 #[test]
11214 fn test_vm_pow_builtin() {
11215 let output = run_output("print(pow(2.0, 10.0))");
11216 assert_eq!(output, vec!["1024.0"]);
11217 }
11218
11219 #[test]
11220 fn test_vm_round_builtin() {
11221 let output = run_output("print(round(3.5))");
11222 assert_eq!(output, vec!["4.0"]);
11223 }
11224
11225 #[test]
11226 fn test_vm_try_catch_runtime_error() {
11227 let output = run_output("try {\n let x = 1 / 0\n} catch e {\n print(e)\n}");
11228 assert_eq!(output, vec!["Division by zero"]);
11229 }
11230
11231 #[test]
11232 fn test_vm_struct_field_access() {
11233 let output = run_output(
11234 "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.5, y: 2.5 }\nprint(p.x)",
11235 );
11236 assert_eq!(output, vec!["1.5"]);
11237 }
11238
11239 #[test]
11240 fn test_vm_enum_match() {
11241 let output = run_output(
11242 "enum Dir { North, South }\nlet d = Dir::North\nmatch d { Dir::North => print(\"north\"), _ => print(\"other\") }",
11243 );
11244 assert!(!output.is_empty());
11246 }
11247
11248 #[test]
11249 fn test_vm_impl_method_with_args() {
11250 let output = run_output(
11251 "struct Rect { w: float64, h: float64 }\nimpl Rect {\n fn area(self) { self.w * self.h }\n}\nlet r = Rect { w: 3.0, h: 4.0 }\nprint(r.area())",
11252 );
11253 assert_eq!(output, vec!["12.0"]);
11254 }
11255
11256 #[test]
11257 fn test_vm_string_len() {
11258 let output = run_output("print(\"hello\".len())");
11259 assert_eq!(output, vec!["5"]);
11260 }
11261
11262 #[test]
11263 fn test_vm_list_reduce() {
11264 let output = run_output(
11265 "let nums = [1, 2, 3, 4]\nlet s = nums.reduce(0, (acc, x) => acc + x)\nprint(s)",
11266 );
11267 assert_eq!(output, vec!["10"]);
11268 }
11269
11270 #[test]
11271 fn test_vm_nested_try_catch() {
11272 let output = run_output(
11273 "try {\n try {\n throw \"inner\"\n } catch e {\n print(e)\n throw \"outer\"\n }\n} catch e2 {\n print(e2)\n}",
11274 );
11275 assert_eq!(output, vec!["inner", "outer"]);
11276 }
11277
11278 #[test]
11279 fn test_vm_math_pow() {
11280 let output = run_output("print(pow(2.0, 10.0))");
11281 assert_eq!(output, vec!["1024.0"]);
11282 }
11283
11284 #[test]
11287 fn test_vm_json_parse() {
11288 let output = run_output(
11289 r#"let m = map_from("a", 1, "b", "hello")
11290let s = json_stringify(m)
11291let m2 = json_parse(s)
11292print(m2["a"])
11293print(m2["b"])"#,
11294 );
11295 assert_eq!(output, vec!["1", "hello"]);
11296 }
11297
11298 #[test]
11299 fn test_vm_json_stringify() {
11300 let output = run_output(
11301 r#"let m = map_from("x", 1, "y", 2)
11302let s = json_stringify(m)
11303print(s)"#,
11304 );
11305 assert_eq!(output, vec![r#"{"x":1,"y":2}"#]);
11306 }
11307
11308 #[test]
11309 fn test_vm_map_from_and_access() {
11310 let output = run_output(
11311 r#"let m = map_from("a", 10, "b", 20)
11312print(m["a"])
11313print(m.b)"#,
11314 );
11315 assert_eq!(output, vec!["10", "20"]);
11316 }
11317
11318 #[test]
11319 fn test_vm_map_methods() {
11320 let output = run_output(
11321 r#"let m = map_from("a", 1, "b", 2)
11322print(m.keys())
11323print(m.values())
11324print(m.contains_key("a"))
11325print(m.len())"#,
11326 );
11327 assert_eq!(output, vec!["[a, b]", "[1, 2]", "true", "2"]);
11328 }
11329
11330 #[test]
11331 fn test_vm_map_set_index() {
11332 let output = run_output(
11333 r#"let m = map_from("a", 1)
11334m["b"] = 2
11335print(m["b"])"#,
11336 );
11337 assert_eq!(output, vec!["2"]);
11338 }
11339
11340 #[test]
11341 fn test_vm_map_iteration() {
11342 let output = run_output(
11343 r#"let m = map_from("x", 10, "y", 20)
11344for kv in m {
11345 print(kv[0])
11346}"#,
11347 );
11348 assert_eq!(output, vec!["x", "y"]);
11349 }
11350
11351 #[test]
11352 fn test_vm_file_read_write() {
11353 let output = run_output(
11354 r#"write_file("/tmp/tl_vm_test.txt", "vm hello")
11355print(read_file("/tmp/tl_vm_test.txt"))
11356print(file_exists("/tmp/tl_vm_test.txt"))"#,
11357 );
11358 assert_eq!(output, vec!["vm hello", "true"]);
11359 }
11360
11361 #[test]
11362 fn test_vm_env_get_set() {
11363 let output = run_output(
11364 r#"env_set("TL_VM_TEST", "abc")
11365print(env_get("TL_VM_TEST"))"#,
11366 );
11367 assert_eq!(output, vec!["abc"]);
11368 }
11369
11370 #[test]
11371 fn test_vm_regex_match() {
11372 let output = run_output(
11373 r#"print(regex_match("\\d+", "abc123"))
11374print(regex_match("^\\d+$", "abc"))"#,
11375 );
11376 assert_eq!(output, vec!["true", "false"]);
11377 }
11378
11379 #[test]
11380 fn test_vm_regex_find() {
11381 let output = run_output(
11382 r#"let m = regex_find("\\d+", "abc123def456")
11383print(len(m))
11384print(m[0])"#,
11385 );
11386 assert_eq!(output, vec!["2", "123"]);
11387 }
11388
11389 #[test]
11390 fn test_vm_regex_replace() {
11391 let output = run_output(r#"print(regex_replace("\\d+", "abc123", "X"))"#);
11392 assert_eq!(output, vec!["abcX"]);
11393 }
11394
11395 #[test]
11396 fn test_vm_now() {
11397 let output = run_output("let t = now()\nprint(type_of(t))");
11399 assert_eq!(output, vec!["datetime"]);
11400 }
11401
11402 #[test]
11403 fn test_vm_date_format() {
11404 let output = run_output(r#"print(date_format(1704067200000, "%Y-%m-%d"))"#);
11405 assert_eq!(output, vec!["2024-01-01"]);
11406 }
11407
11408 #[test]
11409 fn test_vm_date_parse() {
11410 let output = run_output(r#"print(date_parse("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))"#);
11411 assert_eq!(output, vec!["2024-01-01 00:00:00"]);
11412 }
11413
11414 #[test]
11415 fn test_vm_string_chars() {
11416 let output = run_output(r#"print(len("hello".chars()))"#);
11417 assert_eq!(output, vec!["5"]);
11418 }
11419
11420 #[test]
11421 fn test_vm_string_repeat() {
11422 let output = run_output(r#"print("ab".repeat(3))"#);
11423 assert_eq!(output, vec!["ababab"]);
11424 }
11425
11426 #[test]
11427 fn test_vm_string_index_of() {
11428 let output = run_output(r#"print("hello world".index_of("world"))"#);
11429 assert_eq!(output, vec!["6"]);
11430 }
11431
11432 #[test]
11433 fn test_vm_string_substring() {
11434 let output = run_output(r#"print("hello world".substring(0, 5))"#);
11435 assert_eq!(output, vec!["hello"]);
11436 }
11437
11438 #[test]
11439 fn test_vm_string_pad() {
11440 let output = run_output(
11441 r#"print("42".pad_left(5, "0"))
11442print("hi".pad_right(5, "."))"#,
11443 );
11444 assert_eq!(output, vec!["00042", "hi..."]);
11445 }
11446
11447 #[test]
11448 fn test_vm_list_sort() {
11449 let output = run_output(r#"print([3, 1, 2].sort())"#);
11450 assert_eq!(output, vec!["[1, 2, 3]"]);
11451 }
11452
11453 #[test]
11454 fn test_vm_list_reverse() {
11455 let output = run_output(r#"print([1, 2, 3].reverse())"#);
11456 assert_eq!(output, vec!["[3, 2, 1]"]);
11457 }
11458
11459 #[test]
11460 fn test_vm_list_contains() {
11461 let output = run_output(
11462 r#"print([1, 2, 3].contains(2))
11463print([1, 2, 3].contains(5))"#,
11464 );
11465 assert_eq!(output, vec!["true", "false"]);
11466 }
11467
11468 #[test]
11469 fn test_vm_list_slice() {
11470 let output = run_output(r#"print([1, 2, 3, 4, 5].slice(1, 4))"#);
11471 assert_eq!(output, vec!["[2, 3, 4]"]);
11472 }
11473
11474 #[test]
11475 fn test_vm_zip() {
11476 let output = run_output(
11477 r#"let p = zip([1, 2], ["a", "b"])
11478print(p[0])"#,
11479 );
11480 assert_eq!(output, vec!["[1, a]"]);
11481 }
11482
11483 #[test]
11484 fn test_vm_enumerate() {
11485 let output = run_output(
11486 r#"let e = enumerate(["a", "b", "c"])
11487print(e[1])"#,
11488 );
11489 assert_eq!(output, vec!["[1, b]"]);
11490 }
11491
11492 #[test]
11493 fn test_vm_bool() {
11494 let output = run_output(
11495 r#"print(bool(1))
11496print(bool(0))
11497print(bool(""))"#,
11498 );
11499 assert_eq!(output, vec!["true", "false", "false"]);
11500 }
11501
11502 #[test]
11503 fn test_vm_range_step() {
11504 let output = run_output(r#"print(range(0, 10, 3))"#);
11505 assert_eq!(output, vec!["[0, 3, 6, 9]"]);
11506 }
11507
11508 #[test]
11509 fn test_vm_int_bool() {
11510 let output = run_output(
11511 r#"print(int(true))
11512print(int(false))"#,
11513 );
11514 assert_eq!(output, vec!["1", "0"]);
11515 }
11516
11517 #[test]
11518 fn test_vm_map_len_typeof() {
11519 let output = run_output(
11520 r#"let m = map_from("a", 1)
11521print(len(m))
11522print(type_of(m))"#,
11523 );
11524 assert_eq!(output, vec!["1", "map"]);
11525 }
11526
11527 #[test]
11528 fn test_vm_json_file_roundtrip() {
11529 let output = run_output(
11530 r#"let data = map_from("name", "vm_test", "count", 99)
11531write_file("/tmp/tl_vm_json.json", json_stringify(data))
11532let parsed = json_parse(read_file("/tmp/tl_vm_json.json"))
11533print(parsed["name"])
11534print(parsed["count"])"#,
11535 );
11536 assert_eq!(output, vec!["vm_test", "99"]);
11537 }
11538
11539 #[test]
11542 fn test_vm_spawn_await_basic() {
11543 let output = run_output(
11544 r#"fn worker() { 42 }
11545let t = spawn(worker)
11546let result = await t
11547print(result)"#,
11548 );
11549 assert_eq!(output, vec!["42"]);
11550 }
11551
11552 #[test]
11553 fn test_vm_spawn_closure_with_capture() {
11554 let output = run_output(
11555 r#"let x = 10
11556let f = () => x + 5
11557let t = spawn(f)
11558print(await t)"#,
11559 );
11560 assert_eq!(output, vec!["15"]);
11561 }
11562
11563 #[test]
11564 fn test_vm_sleep() {
11565 let output = run_output(
11566 r#"sleep(10)
11567print("done")"#,
11568 );
11569 assert_eq!(output, vec!["done"]);
11570 }
11571
11572 #[test]
11573 fn test_vm_await_non_task_passthrough() {
11574 let output = run_output(r#"print(await 42)"#);
11575 assert_eq!(output, vec!["42"]);
11576 }
11577
11578 #[test]
11579 fn test_vm_spawn_multiple_await() {
11580 let output = run_output(
11581 r#"fn w1() { 1 }
11582fn w2() { 2 }
11583fn w3() { 3 }
11584let t1 = spawn(w1)
11585let t2 = spawn(w2)
11586let t3 = spawn(w3)
11587let a = await t1
11588let b = await t2
11589let c = await t3
11590print(a + b + c)"#,
11591 );
11592 assert_eq!(output, vec!["6"]);
11593 }
11594
11595 #[test]
11596 fn test_vm_channel_basic() {
11597 let output = run_output(
11598 r#"let ch = channel()
11599send(ch, 42)
11600let val = recv(ch)
11601print(val)"#,
11602 );
11603 assert_eq!(output, vec!["42"]);
11604 }
11605
11606 #[test]
11607 fn test_vm_channel_between_tasks() {
11608 let output = run_output(
11609 r#"let ch = channel()
11610fn producer() { send(ch, 100) }
11611let t = spawn(producer)
11612let val = recv(ch)
11613await t
11614print(val)"#,
11615 );
11616 assert_eq!(output, vec!["100"]);
11617 }
11618
11619 #[test]
11620 fn test_vm_try_recv_empty() {
11621 let output = run_output(
11622 r#"let ch = channel()
11623let val = try_recv(ch)
11624print(val)"#,
11625 );
11626 assert_eq!(output, vec!["none"]);
11627 }
11628
11629 #[test]
11630 fn test_vm_channel_multiple_values() {
11631 let output = run_output(
11632 r#"let ch = channel()
11633send(ch, 1)
11634send(ch, 2)
11635send(ch, 3)
11636print(recv(ch))
11637print(recv(ch))
11638print(recv(ch))"#,
11639 );
11640 assert_eq!(output, vec!["1", "2", "3"]);
11641 }
11642
11643 #[test]
11644 fn test_vm_channel_producer_consumer() {
11645 let output = run_output(
11646 r#"let ch = channel()
11647fn producer() {
11648 send(ch, 10)
11649 send(ch, 20)
11650 send(ch, 30)
11651}
11652let t = spawn(producer)
11653let a = recv(ch)
11654let b = recv(ch)
11655let c = recv(ch)
11656await t
11657print(a + b + c)"#,
11658 );
11659 assert_eq!(output, vec!["60"]);
11660 }
11661
11662 #[test]
11663 fn test_vm_await_all() {
11664 let output = run_output(
11665 r#"fn w1() { 10 }
11666fn w2() { 20 }
11667fn w3() { 30 }
11668let t1 = spawn(w1)
11669let t2 = spawn(w2)
11670let t3 = spawn(w3)
11671let results = await_all([t1, t2, t3])
11672print(sum(results))"#,
11673 );
11674 assert_eq!(output, vec!["60"]);
11675 }
11676
11677 #[test]
11678 fn test_vm_pmap_basic() {
11679 let output = run_output(
11680 r#"let results = pmap([1, 2, 3], (x) => x * 2)
11681print(results)"#,
11682 );
11683 assert_eq!(output, vec!["[2, 4, 6]"]);
11684 }
11685
11686 #[test]
11687 fn test_vm_pmap_order_preserved() {
11688 let output = run_output(
11689 r#"let results = pmap([10, 20, 30], (x) => x + 1)
11690print(results)"#,
11691 );
11692 assert_eq!(output, vec!["[11, 21, 31]"]);
11693 }
11694
11695 #[test]
11696 fn test_vm_timeout_success() {
11697 let output = run_output(
11698 r#"fn worker() { 42 }
11699let t = spawn(worker)
11700let result = timeout(t, 5000)
11701print(result)"#,
11702 );
11703 assert_eq!(output, vec!["42"]);
11704 }
11705
11706 #[test]
11707 fn test_vm_timeout_failure() {
11708 let output = run_output(
11709 r#"fn slow() { sleep(10000) }
11710let t = spawn(slow)
11711let result = "ok"
11712try {
11713 result = timeout(t, 50)
11714} catch e {
11715 result = e
11716}
11717print(result)"#,
11718 );
11719 assert_eq!(output, vec!["Task timed out"]);
11720 }
11721
11722 #[test]
11723 fn test_vm_spawn_error_propagation() {
11724 let output = run_output(
11725 r#"fn bad() { throw "bad thing" }
11726let result = "ok"
11727try {
11728 let t = spawn(bad)
11729 result = await t
11730} catch e {
11731 result = e
11732}
11733print(result)"#,
11734 );
11735 assert_eq!(output, vec!["bad thing"]);
11736 }
11737
11738 #[test]
11739 fn test_vm_spawn_producer_consumer_pipeline() {
11740 let output = run_output(
11741 r#"let ch = channel()
11742fn producer() {
11743 let mut i = 0
11744 while i < 5 {
11745 send(ch, i * 10)
11746 i = i + 1
11747 }
11748}
11749let t = spawn(producer)
11750let mut total = 0
11751let mut count = 0
11752while count < 5 {
11753 total = total + recv(ch)
11754 count = count + 1
11755}
11756await t
11757print(total)"#,
11758 );
11759 assert_eq!(output, vec!["100"]);
11760 }
11761
11762 #[test]
11763 fn test_vm_type_of_task_channel() {
11764 let output = run_output(
11765 r#"fn worker() { 1 }
11766let t = spawn(worker)
11767let ch = channel()
11768print(type_of(t))
11769print(type_of(ch))
11770await t"#,
11771 );
11772 assert_eq!(output, vec!["task", "channel"]);
11773 }
11774
11775 #[test]
11778 fn test_vm_basic_generator() {
11779 let output = run_output(
11780 r#"fn gen() {
11781 yield 1
11782 yield 2
11783 yield 3
11784}
11785let g = gen()
11786print(next(g))
11787print(next(g))
11788print(next(g))
11789print(next(g))"#,
11790 );
11791 assert_eq!(output, vec!["1", "2", "3", "none"]);
11792 }
11793
11794 #[test]
11795 fn test_vm_generator_exhaustion() {
11796 let output = run_output(
11797 r#"fn gen() {
11798 yield 42
11799}
11800let g = gen()
11801print(next(g))
11802print(next(g))
11803print(next(g))"#,
11804 );
11805 assert_eq!(output, vec!["42", "none", "none"]);
11806 }
11807
11808 #[test]
11809 fn test_vm_generator_with_loop() {
11810 let output = run_output(
11811 r#"fn counter() {
11812 let mut i = 0
11813 while i < 3 {
11814 yield i
11815 i = i + 1
11816 }
11817}
11818let g = counter()
11819print(next(g))
11820print(next(g))
11821print(next(g))
11822print(next(g))"#,
11823 );
11824 assert_eq!(output, vec!["0", "1", "2", "none"]);
11825 }
11826
11827 #[test]
11828 fn test_vm_generator_with_args() {
11829 let output = run_output(
11830 r#"fn count_from(start) {
11831 let mut i = start
11832 while i < start + 3 {
11833 yield i
11834 i = i + 1
11835 }
11836}
11837let g = count_from(10)
11838print(next(g))
11839print(next(g))
11840print(next(g))
11841print(next(g))"#,
11842 );
11843 assert_eq!(output, vec!["10", "11", "12", "none"]);
11844 }
11845
11846 #[test]
11847 fn test_vm_generator_yield_none() {
11848 let output = run_output(
11849 r#"fn gen() {
11850 yield
11851 yield 5
11852}
11853let g = gen()
11854print(next(g))
11855print(next(g))
11856print(next(g))"#,
11857 );
11858 assert_eq!(output, vec!["none", "5", "none"]);
11859 }
11860
11861 #[test]
11862 fn test_vm_is_generator() {
11863 let output = run_output(
11864 r#"fn gen() { yield 1 }
11865let g = gen()
11866print(is_generator(g))
11867print(is_generator(42))
11868print(is_generator(none))"#,
11869 );
11870 assert_eq!(output, vec!["true", "false", "false"]);
11871 }
11872
11873 #[test]
11874 fn test_vm_multiple_generators() {
11875 let output = run_output(
11876 r#"fn gen() {
11877 yield 1
11878 yield 2
11879}
11880let g1 = gen()
11881let g2 = gen()
11882print(next(g1))
11883print(next(g2))
11884print(next(g1))
11885print(next(g2))"#,
11886 );
11887 assert_eq!(output, vec!["1", "1", "2", "2"]);
11888 }
11889
11890 #[test]
11891 fn test_vm_for_over_generator() {
11892 let output = run_output(
11893 r#"fn gen() {
11894 yield 10
11895 yield 20
11896 yield 30
11897}
11898for x in gen() {
11899 print(x)
11900}"#,
11901 );
11902 assert_eq!(output, vec!["10", "20", "30"]);
11903 }
11904
11905 #[test]
11906 fn test_vm_iter_builtin() {
11907 let output = run_output(
11908 r#"let g = iter([1, 2, 3])
11909print(next(g))
11910print(next(g))
11911print(next(g))
11912print(next(g))"#,
11913 );
11914 assert_eq!(output, vec!["1", "2", "3", "none"]);
11915 }
11916
11917 #[test]
11918 fn test_vm_take_builtin() {
11919 let output = run_output(
11920 r#"fn naturals() {
11921 let mut n = 0
11922 while true {
11923 yield n
11924 n = n + 1
11925 }
11926}
11927let g = take(naturals(), 5)
11928print(next(g))
11929print(next(g))
11930print(next(g))
11931print(next(g))
11932print(next(g))
11933print(next(g))"#,
11934 );
11935 assert_eq!(output, vec!["0", "1", "2", "3", "4", "none"]);
11936 }
11937
11938 #[test]
11939 fn test_vm_skip_builtin() {
11940 let output = run_output(
11941 r#"let g = skip(iter([10, 20, 30, 40, 50]), 2)
11942print(next(g))
11943print(next(g))
11944print(next(g))
11945print(next(g))"#,
11946 );
11947 assert_eq!(output, vec!["30", "40", "50", "none"]);
11948 }
11949
11950 #[test]
11951 fn test_vm_gen_collect() {
11952 let output = run_output(
11953 r#"fn gen() {
11954 yield 1
11955 yield 2
11956 yield 3
11957}
11958let result = gen_collect(gen())
11959print(result)"#,
11960 );
11961 assert_eq!(output, vec!["[1, 2, 3]"]);
11962 }
11963
11964 #[test]
11965 fn test_vm_gen_map() {
11966 let output = run_output(
11967 r#"let g = gen_map(iter([1, 2, 3]), (x) => x * 10)
11968print(gen_collect(g))"#,
11969 );
11970 assert_eq!(output, vec!["[10, 20, 30]"]);
11971 }
11972
11973 #[test]
11974 fn test_vm_gen_filter() {
11975 let output = run_output(
11976 r#"let g = gen_filter(iter([1, 2, 3, 4, 5, 6]), (x) => x % 2 == 0)
11977print(gen_collect(g))"#,
11978 );
11979 assert_eq!(output, vec!["[2, 4, 6]"]);
11980 }
11981
11982 #[test]
11983 fn test_vm_chain() {
11984 let output = run_output(
11985 r#"let g = chain(iter([1, 2]), iter([3, 4]))
11986print(gen_collect(g))"#,
11987 );
11988 assert_eq!(output, vec!["[1, 2, 3, 4]"]);
11989 }
11990
11991 #[test]
11992 fn test_vm_gen_zip() {
11993 let output = run_output(
11994 r#"let g = gen_zip(iter([1, 2, 3]), iter([10, 20, 30]))
11995print(gen_collect(g))"#,
11996 );
11997 assert_eq!(output, vec!["[[1, 10], [2, 20], [3, 30]]"]);
11998 }
11999
12000 #[test]
12001 fn test_vm_gen_enumerate() {
12002 let output = run_output(
12003 r#"let g = gen_enumerate(iter([10, 20, 30]))
12004print(gen_collect(g))"#,
12005 );
12006 assert_eq!(output, vec!["[[0, 10], [1, 20], [2, 30]]"]);
12007 }
12008
12009 #[test]
12010 fn test_vm_combinator_chaining() {
12011 let output = run_output(
12012 r#"fn naturals() {
12013 let mut n = 0
12014 while true {
12015 yield n
12016 n = n + 1
12017 }
12018}
12019let result = gen_collect(gen_map(gen_filter(take(naturals(), 10), (x) => x % 2 == 0), (x) => x * x))
12020print(result)"#,
12021 );
12022 assert_eq!(output, vec!["[0, 4, 16, 36, 64]"]);
12023 }
12024
12025 #[test]
12026 fn test_vm_for_over_take() {
12027 let output = run_output(
12028 r#"fn naturals() {
12029 let mut n = 0
12030 while true {
12031 yield n
12032 n = n + 1
12033 }
12034}
12035for x in take(naturals(), 5) {
12036 print(x)
12037}"#,
12038 );
12039 assert_eq!(output, vec!["0", "1", "2", "3", "4"]);
12040 }
12041
12042 #[test]
12043 fn test_vm_generator_error_propagation() {
12044 let result = run(r#"fn bad_gen() {
12045 yield 1
12046 throw "oops"
12047}
12048let g = bad_gen()
12049let mut caught = ""
12050next(g)
12051try {
12052 next(g)
12053} catch e {
12054 caught = e
12055}
12056print(caught)"#);
12057 assert!(result.is_ok());
12059 }
12060
12061 #[test]
12062 fn test_vm_fibonacci_generator() {
12063 let output = run_output(
12064 r#"fn fib() {
12065 let mut a = 0
12066 let mut b = 1
12067 while true {
12068 yield a
12069 let temp = a + b
12070 a = b
12071 b = temp
12072 }
12073}
12074print(gen_collect(take(fib(), 8)))"#,
12075 );
12076 assert_eq!(output, vec!["[0, 1, 1, 2, 3, 5, 8, 13]"]);
12077 }
12078
12079 #[test]
12080 fn test_vm_generator_method_syntax() {
12081 let output = run_output(
12082 r#"fn gen() {
12083 yield 1
12084 yield 2
12085 yield 3
12086}
12087let g = gen()
12088print(type_of(g))"#,
12089 );
12090 assert_eq!(output, vec!["generator"]);
12091 }
12092
12093 #[test]
12096 fn test_vm_ok_err_builtins() {
12097 let output = run_output("let r = Ok(42)\nprint(r)");
12098 assert_eq!(output, vec!["Result::Ok(42)"]);
12099
12100 let output = run_output("let r = Err(\"fail\")\nprint(r)");
12101 assert_eq!(output, vec!["Result::Err(fail)"]);
12102 }
12103
12104 #[test]
12105 fn test_vm_is_ok_is_err() {
12106 let output = run_output("print(is_ok(Ok(42)))");
12107 assert_eq!(output, vec!["true"]);
12108 let output = run_output("print(is_err(Ok(42)))");
12109 assert_eq!(output, vec!["false"]);
12110 let output = run_output("print(is_ok(Err(\"fail\")))");
12111 assert_eq!(output, vec!["false"]);
12112 let output = run_output("print(is_err(Err(\"fail\")))");
12113 assert_eq!(output, vec!["true"]);
12114 }
12115
12116 #[test]
12117 fn test_vm_unwrap_ok() {
12118 let output = run_output("print(unwrap(Ok(42)))");
12119 assert_eq!(output, vec!["42"]);
12120 }
12121
12122 #[test]
12123 fn test_vm_unwrap_err_panics() {
12124 let result = run("unwrap(Err(\"fail\"))");
12125 assert!(result.is_err());
12126 }
12127
12128 #[test]
12129 fn test_vm_try_on_ok() {
12130 let output = run_output(
12131 r#"fn get_val() { Ok(42) }
12132fn process() { let v = get_val()? + 1
12133Ok(v) }
12134print(process())"#,
12135 );
12136 assert_eq!(output, vec!["Result::Ok(43)"]);
12137 }
12138
12139 #[test]
12140 fn test_vm_try_on_err_propagates() {
12141 let output = run_output(
12142 r#"fn failing() { Err("oops") }
12143fn process() { let v = failing()?
12144Ok(v) }
12145print(process())"#,
12146 );
12147 assert_eq!(output, vec!["Result::Err(oops)"]);
12148 }
12149
12150 #[test]
12151 fn test_vm_try_on_none_propagates() {
12152 let output = run_output(
12153 r#"fn get_none() { none }
12154fn process() { let v = get_none()?
1215542 }
12156print(process())"#,
12157 );
12158 assert_eq!(output, vec!["none"]);
12159 }
12160
12161 #[test]
12162 fn test_vm_try_passthrough() {
12163 let output = run_output(
12165 r#"fn get_val() { 42 }
12166fn process() { let v = get_val()?
12167v + 1 }
12168print(process())"#,
12169 );
12170 assert_eq!(output, vec!["43"]);
12171 }
12172
12173 #[test]
12174 fn test_vm_result_match() {
12175 let output = run_output(
12176 r#"let r = Ok(42)
12177print(is_ok(r))
12178print(unwrap(r))"#,
12179 );
12180 assert_eq!(output, vec!["true", "42"]);
12181 }
12182
12183 #[test]
12184 fn test_vm_result_match_err() {
12185 let output = run_output(
12186 r#"let r = Err("fail")
12187print(is_err(r))
12188match r {
12189 Result::Err(e) => print("got error"),
12190 _ => print("no error")
12191}"#,
12192 );
12193 assert_eq!(output, vec!["true", "got error"]);
12194 }
12195
12196 #[test]
12199 fn test_vm_set_from_dedup() {
12200 let output = run_output(
12201 r#"let s = set_from([1, 2, 3, 2, 1])
12202print(len(s))
12203print(type_of(s))"#,
12204 );
12205 assert_eq!(output, vec!["3", "set"]);
12206 }
12207
12208 #[test]
12209 fn test_vm_set_add() {
12210 let output = run_output(
12211 r#"let s = set_from([1, 2])
12212let s2 = set_add(s, 3)
12213let s3 = set_add(s2, 2)
12214print(len(s2))
12215print(len(s3))"#,
12216 );
12217 assert_eq!(output, vec!["3", "3"]);
12218 }
12219
12220 #[test]
12221 fn test_vm_set_remove() {
12222 let output = run_output(
12223 r#"let s = set_from([1, 2, 3])
12224let s2 = set_remove(s, 2)
12225print(len(s2))
12226print(set_contains(s2, 2))"#,
12227 );
12228 assert_eq!(output, vec!["2", "false"]);
12229 }
12230
12231 #[test]
12232 fn test_vm_set_contains() {
12233 let output = run_output(
12234 r#"let s = set_from([1, 2, 3])
12235print(set_contains(s, 2))
12236print(set_contains(s, 5))"#,
12237 );
12238 assert_eq!(output, vec!["true", "false"]);
12239 }
12240
12241 #[test]
12242 fn test_vm_set_union() {
12243 let output = run_output(
12244 r#"let a = set_from([1, 2, 3])
12245let b = set_from([3, 4, 5])
12246let c = set_union(a, b)
12247print(len(c))"#,
12248 );
12249 assert_eq!(output, vec!["5"]);
12250 }
12251
12252 #[test]
12253 fn test_vm_set_intersection() {
12254 let output = run_output(
12255 r#"let a = set_from([1, 2, 3])
12256let b = set_from([2, 3, 4])
12257let c = set_intersection(a, b)
12258print(len(c))"#,
12259 );
12260 assert_eq!(output, vec!["2"]);
12261 }
12262
12263 #[test]
12264 fn test_vm_set_difference() {
12265 let output = run_output(
12266 r#"let a = set_from([1, 2, 3])
12267let b = set_from([2, 3, 4])
12268let c = set_difference(a, b)
12269print(len(c))"#,
12270 );
12271 assert_eq!(output, vec!["1"]);
12272 }
12273
12274 #[test]
12275 fn test_vm_set_for_loop() {
12276 let output = run_output(
12277 r#"let s = set_from([10, 20, 30])
12278let total = 0
12279for item in s {
12280 total = total + item
12281}
12282print(total)"#,
12283 );
12284 assert_eq!(output, vec!["60"]);
12285 }
12286
12287 #[test]
12288 fn test_vm_set_to_list() {
12289 let output = run_output(
12290 r#"let s = set_from([3, 1, 2])
12291let lst = s.to_list()
12292print(type_of(lst))
12293print(len(lst))"#,
12294 );
12295 assert_eq!(output, vec!["list", "3"]);
12296 }
12297
12298 #[test]
12299 fn test_vm_set_method_contains() {
12300 let output = run_output(
12301 r#"let s = set_from([1, 2, 3])
12302print(s.contains(2))
12303print(s.contains(5))"#,
12304 );
12305 assert_eq!(output, vec!["true", "false"]);
12306 }
12307
12308 #[test]
12309 fn test_vm_set_method_add_remove() {
12310 let output = run_output(
12311 r#"let s = set_from([1, 2])
12312let s2 = s.add(3)
12313print(s2.len())
12314let s3 = s2.remove(1)
12315print(s3.len())"#,
12316 );
12317 assert_eq!(output, vec!["3", "2"]);
12318 }
12319
12320 #[test]
12321 fn test_vm_set_method_union_intersection_difference() {
12322 let output = run_output(
12323 r#"let a = set_from([1, 2, 3])
12324let b = set_from([2, 3, 4])
12325print(a.union(b).len())
12326print(a.intersection(b).len())
12327print(a.difference(b).len())"#,
12328 );
12329 assert_eq!(output, vec!["4", "2", "1"]);
12330 }
12331
12332 #[test]
12333 fn test_vm_set_empty() {
12334 let output = run_output(
12335 r#"let s = set_from([])
12336print(len(s))
12337let s2 = s.add(1)
12338print(len(s2))"#,
12339 );
12340 assert_eq!(output, vec!["0", "1"]);
12341 }
12342
12343 #[test]
12344 fn test_vm_set_string_values() {
12345 let output = run_output(
12346 r#"let s = set_from(["a", "b", "a", "c"])
12347print(len(s))
12348print(s.contains("b"))"#,
12349 );
12350 assert_eq!(output, vec!["3", "true"]);
12351 }
12352
12353 #[test]
12356 fn test_vm_import_with_caching() {
12357 let vm = Vm::new();
12359 assert!(vm.module_cache.is_empty());
12360 assert!(vm.importing_files.is_empty());
12361 assert!(vm.file_path.is_none());
12362 }
12363
12364 #[test]
12365 fn test_vm_use_single_file() {
12366 let dir = tempfile::tempdir().unwrap();
12368 let lib_path = dir.path().join("math.tl");
12369 std::fs::write(&lib_path, "let PI = 3.14\nfn add(a, b) { a + b }").unwrap();
12370
12371 let main_path = dir.path().join("main.tl");
12372 std::fs::write(&main_path, "use math\nprint(add(1, 2))").unwrap();
12373
12374 let source = std::fs::read_to_string(&main_path).unwrap();
12375 let program = tl_parser::parse(&source).unwrap();
12376 let proto = crate::compiler::compile(&program).unwrap();
12377
12378 let mut vm = Vm::new();
12379 vm.file_path = Some(main_path.to_string_lossy().to_string());
12380 vm.execute(&proto).unwrap();
12381 assert_eq!(vm.output, vec!["3"]);
12382 }
12383
12384 #[test]
12385 fn test_vm_use_wildcard() {
12386 let dir = tempfile::tempdir().unwrap();
12387 std::fs::write(
12388 dir.path().join("helpers.tl"),
12389 "fn greet() { \"hello\" }\nfn farewell() { \"bye\" }",
12390 )
12391 .unwrap();
12392
12393 let main_src = "use helpers.*\nprint(greet())\nprint(farewell())";
12394 let main_path = dir.path().join("main.tl");
12395 std::fs::write(&main_path, main_src).unwrap();
12396
12397 let program = tl_parser::parse(main_src).unwrap();
12398 let proto = crate::compiler::compile(&program).unwrap();
12399
12400 let mut vm = Vm::new();
12401 vm.file_path = Some(main_path.to_string_lossy().to_string());
12402 vm.execute(&proto).unwrap();
12403 assert_eq!(vm.output, vec!["hello", "bye"]);
12404 }
12405
12406 #[test]
12407 fn test_vm_use_aliased() {
12408 let dir = tempfile::tempdir().unwrap();
12409 std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12410
12411 let main_src = "use mylib as m\nprint(m.compute())";
12412 let main_path = dir.path().join("main.tl");
12413 std::fs::write(&main_path, main_src).unwrap();
12414
12415 let program = tl_parser::parse(main_src).unwrap();
12416 let proto = crate::compiler::compile(&program).unwrap();
12417
12418 let mut vm = Vm::new();
12419 vm.file_path = Some(main_path.to_string_lossy().to_string());
12420 vm.execute(&proto).unwrap();
12421 assert_eq!(vm.output, vec!["42"]);
12422 }
12423
12424 #[test]
12425 fn test_vm_use_directory_module() {
12426 let dir = tempfile::tempdir().unwrap();
12427 std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12428 std::fs::write(dir.path().join("utils/mod.tl"), "fn helper() { 99 }").unwrap();
12429
12430 let main_src = "use utils\nprint(helper())";
12431 let main_path = dir.path().join("main.tl");
12432 std::fs::write(&main_path, main_src).unwrap();
12433
12434 let program = tl_parser::parse(main_src).unwrap();
12435 let proto = crate::compiler::compile(&program).unwrap();
12436
12437 let mut vm = Vm::new();
12438 vm.file_path = Some(main_path.to_string_lossy().to_string());
12439 vm.execute(&proto).unwrap();
12440 assert_eq!(vm.output, vec!["99"]);
12441 }
12442
12443 #[test]
12444 fn test_vm_circular_import_detection() {
12445 let dir = tempfile::tempdir().unwrap();
12446 let a_path = dir.path().join("a.tl");
12447 let b_path = dir.path().join("b.tl");
12448 std::fs::write(&a_path, &format!("import \"{}\"", b_path.to_string_lossy())).unwrap();
12449 std::fs::write(&b_path, &format!("import \"{}\"", a_path.to_string_lossy())).unwrap();
12450
12451 let source = std::fs::read_to_string(&a_path).unwrap();
12452 let program = tl_parser::parse(&source).unwrap();
12453 let proto = crate::compiler::compile(&program).unwrap();
12454
12455 let mut vm = Vm::new();
12456 vm.file_path = Some(a_path.to_string_lossy().to_string());
12457 let result = vm.execute(&proto);
12458 assert!(result.is_err());
12459 assert!(format!("{:?}", result).contains("Circular import"));
12460 }
12461
12462 #[test]
12463 fn test_vm_module_caching() {
12464 let dir = tempfile::tempdir().unwrap();
12466 std::fs::write(dir.path().join("cached.tl"), "let X = 42").unwrap();
12467
12468 let main_src = "use cached\nuse cached\nprint(X)";
12469 let main_path = dir.path().join("main.tl");
12470 std::fs::write(&main_path, main_src).unwrap();
12471
12472 let program = tl_parser::parse(main_src).unwrap();
12473 let proto = crate::compiler::compile(&program).unwrap();
12474
12475 let mut vm = Vm::new();
12476 vm.file_path = Some(main_path.to_string_lossy().to_string());
12477 vm.execute(&proto).unwrap();
12478 assert_eq!(vm.output, vec!["42"]);
12479 }
12480
12481 #[test]
12482 fn test_vm_existing_import_still_works() {
12483 let dir = tempfile::tempdir().unwrap();
12485 let lib_path = dir.path().join("lib.tl");
12486 std::fs::write(&lib_path, "fn imported_fn() { 123 }").unwrap();
12487
12488 let main_src = format!(
12489 "import \"{}\"\nprint(imported_fn())",
12490 lib_path.to_string_lossy()
12491 );
12492 let program = tl_parser::parse(&main_src).unwrap();
12493 let proto = crate::compiler::compile(&program).unwrap();
12494
12495 let mut vm = Vm::new();
12496 vm.execute(&proto).unwrap();
12497 assert_eq!(vm.output, vec!["123"]);
12498 }
12499
12500 #[test]
12501 fn test_vm_pub_fn_parsing() {
12502 let output = run_output("pub fn add(a, b) { a + b }\nprint(add(1, 2))");
12504 assert_eq!(output, vec!["3"]);
12505 }
12506
12507 #[test]
12508 fn test_vm_use_nested_path() {
12509 let dir = tempfile::tempdir().unwrap();
12510 std::fs::create_dir_all(dir.path().join("data")).unwrap();
12511 std::fs::write(
12512 dir.path().join("data/transforms.tl"),
12513 "fn clean(x) { x + 1 }",
12514 )
12515 .unwrap();
12516
12517 let main_src = "use data.transforms\nprint(clean(41))";
12518 let main_path = dir.path().join("main.tl");
12519 std::fs::write(&main_path, main_src).unwrap();
12520
12521 let program = tl_parser::parse(main_src).unwrap();
12522 let proto = crate::compiler::compile(&program).unwrap();
12523
12524 let mut vm = Vm::new();
12525 vm.file_path = Some(main_path.to_string_lossy().to_string());
12526 vm.execute(&proto).unwrap();
12527 assert_eq!(vm.output, vec!["42"]);
12528 }
12529
12530 #[test]
12533 fn test_integration_multi_file_use_functions() {
12534 let dir = tempfile::tempdir().unwrap();
12536 std::fs::write(
12537 dir.path().join("lib.tl"),
12538 "fn greet(name) { \"Hello, \" + name + \"!\" }\nfn double(x) { x * 2 }",
12539 )
12540 .unwrap();
12541
12542 let main_src = "use lib\nprint(greet(\"World\"))\nprint(double(21))";
12543 let main_path = dir.path().join("main.tl");
12544 std::fs::write(&main_path, main_src).unwrap();
12545
12546 let program = tl_parser::parse(main_src).unwrap();
12547 let proto = crate::compiler::compile(&program).unwrap();
12548 let mut vm = Vm::new();
12549 vm.file_path = Some(main_path.to_string_lossy().to_string());
12550 vm.execute(&proto).unwrap();
12551 assert_eq!(vm.output, vec!["Hello, World!", "42"]);
12552 }
12553
12554 #[test]
12555 fn test_integration_mixed_import_and_use() {
12556 let dir = tempfile::tempdir().unwrap();
12558 std::fs::write(dir.path().join("old_lib.tl"), "fn old_fn() { 10 }").unwrap();
12559 std::fs::write(dir.path().join("new_lib.tl"), "fn new_fn() { 20 }").unwrap();
12560
12561 let old_lib_abs = dir.path().join("old_lib.tl").to_string_lossy().to_string();
12562 let main_src = format!("import \"{old_lib_abs}\"\nuse new_lib\nprint(old_fn() + new_fn())");
12563 let main_path = dir.path().join("main.tl");
12564 std::fs::write(&main_path, &main_src).unwrap();
12565
12566 let program = tl_parser::parse(&main_src).unwrap();
12567 let proto = crate::compiler::compile(&program).unwrap();
12568 let mut vm = Vm::new();
12569 vm.file_path = Some(main_path.to_string_lossy().to_string());
12570 vm.execute(&proto).unwrap();
12571 assert_eq!(vm.output, vec!["30"]);
12572 }
12573
12574 #[test]
12575 fn test_integration_directory_module_with_mod_tl() {
12576 let dir = tempfile::tempdir().unwrap();
12578 std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12579 std::fs::write(
12580 dir.path().join("utils/mod.tl"),
12581 "fn helper() { 99 }\nfn format_num(n) { str(n) + \"!\" }",
12582 )
12583 .unwrap();
12584
12585 let main_src = "use utils\nprint(helper())\nprint(format_num(42))";
12586 let main_path = dir.path().join("main.tl");
12587 std::fs::write(&main_path, main_src).unwrap();
12588
12589 let program = tl_parser::parse(main_src).unwrap();
12590 let proto = crate::compiler::compile(&program).unwrap();
12591 let mut vm = Vm::new();
12592 vm.file_path = Some(main_path.to_string_lossy().to_string());
12593 vm.execute(&proto).unwrap();
12594 assert_eq!(vm.output, vec!["99", "42!"]);
12595 }
12596
12597 #[test]
12598 fn test_integration_circular_dep_error() {
12599 let dir = tempfile::tempdir().unwrap();
12600 let a_abs = dir.path().join("a.tl").to_string_lossy().to_string();
12601 let b_abs = dir.path().join("b.tl").to_string_lossy().to_string();
12602 std::fs::write(
12603 dir.path().join("a.tl"),
12604 format!("import \"{b_abs}\"\nfn fa() {{ 1 }}"),
12605 )
12606 .unwrap();
12607 std::fs::write(
12608 dir.path().join("b.tl"),
12609 format!("import \"{a_abs}\"\nfn fb() {{ 2 }}"),
12610 )
12611 .unwrap();
12612
12613 let main_src = format!("import \"{a_abs}\"");
12614 let program = tl_parser::parse(&main_src).unwrap();
12615 let proto = crate::compiler::compile(&program).unwrap();
12616 let mut vm = Vm::new();
12617 let result = vm.execute(&proto);
12618 assert!(result.is_err());
12619 let err_msg = format!("{}", result.unwrap_err());
12620 assert!(
12621 err_msg.contains("Circular") || err_msg.contains("circular"),
12622 "Expected circular import error, got: {err_msg}"
12623 );
12624 }
12625
12626 #[test]
12627 fn test_integration_use_aliased_method_call() {
12628 let dir = tempfile::tempdir().unwrap();
12630 std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12631
12632 let main_src = "use mylib as m\nprint(m.compute())";
12633 let main_path = dir.path().join("main.tl");
12634 std::fs::write(&main_path, main_src).unwrap();
12635
12636 let program = tl_parser::parse(main_src).unwrap();
12637 let proto = crate::compiler::compile(&program).unwrap();
12638 let mut vm = Vm::new();
12639 vm.file_path = Some(main_path.to_string_lossy().to_string());
12640 vm.execute(&proto).unwrap();
12641 assert_eq!(vm.output, vec!["42"]);
12642 }
12643
12644 #[test]
12645 fn test_integration_module_caching_shared() {
12646 let dir = tempfile::tempdir().unwrap();
12648 std::fs::write(dir.path().join("shared.tl"), "fn get_val() { 42 }").unwrap();
12649
12650 let main_src = "use shared\nprint(get_val())\nuse shared\nprint(get_val())";
12651 let main_path = dir.path().join("main.tl");
12652 std::fs::write(&main_path, main_src).unwrap();
12653
12654 let program = tl_parser::parse(main_src).unwrap();
12655 let proto = crate::compiler::compile(&program).unwrap();
12656 let mut vm = Vm::new();
12657 vm.file_path = Some(main_path.to_string_lossy().to_string());
12658 vm.execute(&proto).unwrap();
12659 assert_eq!(vm.output, vec!["42", "42"]);
12660 }
12661
12662 #[test]
12663 fn test_integration_pub_keyword_in_module() {
12664 let dir = tempfile::tempdir().unwrap();
12666 std::fs::write(
12667 dir.path().join("pubmod.tl"),
12668 "pub fn public_fn() { 100 }\nfn private_fn() { 200 }",
12669 )
12670 .unwrap();
12671
12672 let main_src = "use pubmod\nprint(public_fn())";
12673 let main_path = dir.path().join("main.tl");
12674 std::fs::write(&main_path, main_src).unwrap();
12675
12676 let program = tl_parser::parse(main_src).unwrap();
12677 let proto = crate::compiler::compile(&program).unwrap();
12678 let mut vm = Vm::new();
12679 vm.file_path = Some(main_path.to_string_lossy().to_string());
12680 vm.execute(&proto).unwrap();
12681 assert_eq!(vm.output, vec!["100"]);
12682 }
12683
12684 #[test]
12685 fn test_integration_backward_compat_import_as() {
12686 let dir = tempfile::tempdir().unwrap();
12688 let lib_path = dir.path().join("mylib.tl");
12689 std::fs::write(&lib_path, "fn compute() { 77 }").unwrap();
12690
12691 let main_src = format!(
12692 "import \"{}\" as m\nprint(m.compute())",
12693 lib_path.to_string_lossy()
12694 );
12695 let program = tl_parser::parse(&main_src).unwrap();
12696 let proto = crate::compiler::compile(&program).unwrap();
12697 let mut vm = Vm::new();
12698 vm.execute(&proto).unwrap();
12699 assert_eq!(vm.output, vec!["77"]);
12700 }
12701
12702 #[test]
12705 fn test_vm_generic_fn() {
12706 let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(42))");
12707 assert_eq!(output, vec!["42"]);
12708 }
12709
12710 #[test]
12711 fn test_vm_generic_fn_string() {
12712 let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(\"hello\"))");
12713 assert_eq!(output, vec!["hello"]);
12714 }
12715
12716 #[test]
12717 fn test_vm_generic_struct() {
12718 let output = run_output(
12719 "struct Pair<A, B> { first: A, second: B }\nlet p = Pair { first: 1, second: \"hi\" }\nprint(p.first)\nprint(p.second)",
12720 );
12721 assert_eq!(output, vec!["1", "hi"]);
12722 }
12723
12724 #[test]
12725 fn test_vm_trait_def_noop() {
12726 let output = run_output("trait Display { fn show(self) -> string }\nprint(\"ok\")");
12728 assert_eq!(output, vec!["ok"]);
12729 }
12730
12731 #[test]
12732 fn test_vm_trait_impl_methods() {
12733 let output = run_output(
12734 "struct Point { x: int, y: int }\nimpl Display for Point { fn show(self) -> string { \"point\" } }\nlet p = Point { x: 1, y: 2 }\nprint(p.show())",
12735 );
12736 assert_eq!(output, vec!["point"]);
12737 }
12738
12739 #[test]
12740 fn test_vm_generic_enum() {
12741 let output = run_output(
12743 "enum MyOpt<T> { Some(T), Nothing }\nlet x = MyOpt::Some(42)\nprint(type_of(x))",
12744 );
12745 assert_eq!(output, vec!["enum"]);
12746 }
12747
12748 #[test]
12749 fn test_vm_where_clause_runtime() {
12750 let output =
12752 run_output("fn compare<T>(x: T) where T: Comparable { x }\nprint(compare(10))");
12753 assert_eq!(output, vec!["10"]);
12754 }
12755
12756 #[test]
12757 fn test_vm_trait_impl_self_method() {
12758 let output = run_output(
12759 "struct Counter { value: int }\nimpl Incrementable for Counter { fn inc(self) { self.value + 1 } }\nlet c = Counter { value: 5 }\nprint(c.inc())",
12760 );
12761 assert_eq!(output, vec!["6"]);
12762 }
12763
12764 #[test]
12767 fn test_vm_generic_fn_with_type_inference() {
12768 let output = run_output(
12770 "fn first<T>(xs: list<T>) -> T { xs[0] }\nprint(first([1, 2, 3]))\nprint(first([\"a\", \"b\"]))",
12771 );
12772 assert_eq!(output, vec!["1", "a"]);
12773 }
12774
12775 #[test]
12776 fn test_vm_generic_struct_with_methods() {
12777 let output = run_output(
12778 "struct Box<T> { val: T }\nimpl Box { fn get(self) { self.val } }\nlet b = Box { val: 42 }\nprint(b.get())",
12779 );
12780 assert_eq!(output, vec!["42"]);
12781 }
12782
12783 #[test]
12784 fn test_vm_trait_def_impl_call() {
12785 let output = run_output(
12786 "trait Greetable { fn greet(self) -> string }\nstruct Person { name: string }\nimpl Greetable for Person { fn greet(self) -> string { self.name } }\nlet p = Person { name: \"Alice\" }\nprint(p.greet())",
12787 );
12788 assert_eq!(output, vec!["Alice"]);
12789 }
12790
12791 #[test]
12792 fn test_vm_multiple_generic_params() {
12793 let output = run_output(
12794 "fn pair<A, B>(a: A, b: B) { [a, b] }\nlet p = pair(1, \"two\")\nprint(len(p))",
12795 );
12796 assert_eq!(output, vec!["2"]);
12797 }
12798
12799 #[test]
12800 fn test_vm_backward_compat_non_generic() {
12801 let output = run_output(
12803 "fn add(a, b) { a + b }\nstruct Point { x: int, y: int }\nimpl Point { fn sum(self) { self.x + self.y } }\nlet p = Point { x: 3, y: 4 }\nprint(add(1, 2))\nprint(p.sum())",
12804 );
12805 assert_eq!(output, vec!["3", "7"]);
12806 }
12807
12808 #[test]
12811 fn test_vm_package_import_resolves() {
12812 let tmp = tempfile::tempdir().unwrap();
12814 let pkg_dir = tmp.path().join("mylib");
12815 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12816 std::fs::write(
12817 pkg_dir.join("src/lib.tl"),
12818 "pub fn greet() { print(\"hello from pkg\") }",
12819 )
12820 .unwrap();
12821 std::fs::write(
12822 pkg_dir.join("tl.toml"),
12823 "[project]\nname = \"mylib\"\nversion = \"1.0.0\"\n",
12824 )
12825 .unwrap();
12826
12827 let main_file = tmp.path().join("main.tl");
12829 std::fs::write(&main_file, "use mylib\ngreet()").unwrap();
12830
12831 let source = std::fs::read_to_string(&main_file).unwrap();
12832 let program = tl_parser::parse(&source).unwrap();
12833 let proto = crate::compiler::compile(&program).unwrap();
12834
12835 let mut vm = Vm::new();
12836 vm.file_path = Some(main_file.to_string_lossy().to_string());
12837 vm.package_roots.insert("mylib".into(), pkg_dir);
12838 vm.execute(&proto).unwrap();
12839
12840 assert_eq!(vm.output, vec!["hello from pkg"]);
12841 }
12842
12843 #[test]
12844 fn test_vm_package_nested_import() {
12845 let tmp = tempfile::tempdir().unwrap();
12846 let pkg_dir = tmp.path().join("utils");
12847 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12848 std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12849 std::fs::write(
12850 pkg_dir.join("tl.toml"),
12851 "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12852 )
12853 .unwrap();
12854
12855 let main_file = tmp.path().join("main.tl");
12857 std::fs::write(&main_file, "use utils.math\nprint(double(21))").unwrap();
12858
12859 let source = std::fs::read_to_string(&main_file).unwrap();
12860 let program = tl_parser::parse(&source).unwrap();
12861 let proto = crate::compiler::compile(&program).unwrap();
12862
12863 let mut vm = Vm::new();
12864 vm.file_path = Some(main_file.to_string_lossy().to_string());
12865 vm.package_roots.insert("utils".into(), pkg_dir);
12866 vm.execute(&proto).unwrap();
12867
12868 assert_eq!(vm.output, vec!["42"]);
12869 }
12870
12871 #[test]
12872 fn test_vm_package_aliased_import() {
12873 let tmp = tempfile::tempdir().unwrap();
12874 let pkg_dir = tmp.path().join("utils");
12875 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12876 std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12877 std::fs::write(
12878 pkg_dir.join("tl.toml"),
12879 "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12880 )
12881 .unwrap();
12882
12883 let main_file = tmp.path().join("main.tl");
12885 std::fs::write(&main_file, "use utils.math as m\nprint(m.double(21))").unwrap();
12886
12887 let source = std::fs::read_to_string(&main_file).unwrap();
12888 let program = tl_parser::parse(&source).unwrap();
12889 let proto = crate::compiler::compile(&program).unwrap();
12890
12891 let mut vm = Vm::new();
12892 vm.file_path = Some(main_file.to_string_lossy().to_string());
12893 vm.package_roots.insert("utils".into(), pkg_dir);
12894 vm.execute(&proto).unwrap();
12895
12896 assert_eq!(vm.output, vec!["42"]);
12897 }
12898
12899 #[test]
12900 fn test_vm_package_underscore_to_hyphen() {
12901 let tmp = tempfile::tempdir().unwrap();
12902 let pkg_dir = tmp.path().join("my-pkg");
12903 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12904 std::fs::write(pkg_dir.join("src/lib.tl"), "pub fn val() { print(99) }").unwrap();
12905 std::fs::write(
12906 pkg_dir.join("tl.toml"),
12907 "[project]\nname = \"my-pkg\"\nversion = \"1.0.0\"\n",
12908 )
12909 .unwrap();
12910
12911 let main_file = tmp.path().join("main.tl");
12913 std::fs::write(&main_file, "use my_pkg\nval()").unwrap();
12914
12915 let source = std::fs::read_to_string(&main_file).unwrap();
12916 let program = tl_parser::parse(&source).unwrap();
12917 let proto = crate::compiler::compile(&program).unwrap();
12918
12919 let mut vm = Vm::new();
12920 vm.file_path = Some(main_file.to_string_lossy().to_string());
12921 vm.package_roots.insert("my-pkg".into(), pkg_dir);
12922 vm.execute(&proto).unwrap();
12923
12924 assert_eq!(vm.output, vec!["99"]);
12925 }
12926
12927 #[test]
12928 fn test_vm_local_module_priority_over_package() {
12929 let tmp = tempfile::tempdir().unwrap();
12931
12932 std::fs::write(
12934 tmp.path().join("mymod.tl"),
12935 "pub fn val() { print(\"local\") }",
12936 )
12937 .unwrap();
12938
12939 let pkg_dir = tmp.path().join("pkg_mymod");
12941 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12942 std::fs::write(
12943 pkg_dir.join("src/lib.tl"),
12944 "pub fn val() { print(\"package\") }",
12945 )
12946 .unwrap();
12947
12948 let main_file = tmp.path().join("main.tl");
12950 std::fs::write(&main_file, "use mymod\nval()").unwrap();
12951
12952 let source = std::fs::read_to_string(&main_file).unwrap();
12953 let program = tl_parser::parse(&source).unwrap();
12954 let proto = crate::compiler::compile(&program).unwrap();
12955
12956 let mut vm = Vm::new();
12957 vm.file_path = Some(main_file.to_string_lossy().to_string());
12958 vm.package_roots.insert("mymod".into(), pkg_dir);
12959 vm.execute(&proto).unwrap();
12960
12961 assert_eq!(vm.output, vec!["local"]);
12963 }
12964
12965 #[test]
12966 fn test_vm_package_missing_error() {
12967 let tmp = tempfile::tempdir().unwrap();
12968 let main_file = tmp.path().join("main.tl");
12969 std::fs::write(&main_file, "use nonexistent\nnonexistent.foo()").unwrap();
12970
12971 let source = std::fs::read_to_string(&main_file).unwrap();
12972 let program = tl_parser::parse(&source).unwrap();
12973 let proto = crate::compiler::compile(&program).unwrap();
12974
12975 let mut vm = Vm::new();
12976 vm.file_path = Some(main_file.to_string_lossy().to_string());
12977 let result = vm.execute(&proto);
12978
12979 assert!(result.is_err());
12980 let err = format!("{:?}", result.unwrap_err());
12981 assert!(err.contains("Module not found"));
12982 }
12983
12984 #[test]
12985 #[cfg(feature = "native")]
12986 fn test_resolve_package_file_entry_points() {
12987 let tmp = tempfile::tempdir().unwrap();
12988
12989 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
12991 std::fs::write(tmp.path().join("src/lib.tl"), "").unwrap();
12992 let result = resolve_package_file(tmp.path(), &[]);
12993 assert!(result.is_some());
12994 assert!(result.unwrap().contains("lib.tl"));
12995
12996 std::fs::write(tmp.path().join("src/math.tl"), "").unwrap();
12998 let result = resolve_package_file(tmp.path(), &["math"]);
12999 assert!(result.is_some());
13000 assert!(result.unwrap().contains("math.tl"));
13001 }
13002
13003 #[test]
13004 fn test_vm_package_propagates_to_sub_imports() {
13005 let tmp = tempfile::tempdir().unwrap();
13007
13008 let pkg_dir = tmp.path().join("helpers");
13010 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13011 std::fs::write(
13012 pkg_dir.join("src/lib.tl"),
13013 "pub fn help() { print(\"helped\") }",
13014 )
13015 .unwrap();
13016 std::fs::write(
13017 pkg_dir.join("tl.toml"),
13018 "[project]\nname = \"helpers\"\nversion = \"1.0.0\"\n",
13019 )
13020 .unwrap();
13021
13022 std::fs::write(
13024 tmp.path().join("bridge.tl"),
13025 "use helpers\npub fn run() { help() }",
13026 )
13027 .unwrap();
13028
13029 let main_file = tmp.path().join("main.tl");
13031 std::fs::write(&main_file, "use bridge\nrun()").unwrap();
13032
13033 let source = std::fs::read_to_string(&main_file).unwrap();
13034 let program = tl_parser::parse(&source).unwrap();
13035 let proto = crate::compiler::compile(&program).unwrap();
13036
13037 let mut vm = Vm::new();
13038 vm.file_path = Some(main_file.to_string_lossy().to_string());
13039 vm.package_roots.insert("helpers".into(), pkg_dir);
13040 vm.execute(&proto).unwrap();
13041
13042 assert_eq!(vm.output, vec!["helped"]);
13043 }
13044
13045 #[test]
13048 fn test_block_body_closure_basic() {
13049 let output =
13050 run_output("let f = (x: int64) -> int64 { let y = x * 2\n y + 1 }\nprint(f(5))");
13051 assert_eq!(output, vec!["11"]);
13052 }
13053
13054 #[test]
13055 fn test_block_body_closure_captures_upvalue() {
13056 let output = run_output(
13057 "let offset = 10\nlet f = (x) -> int64 { let y = x + offset\n y }\nprint(f(5))",
13058 );
13059 assert_eq!(output, vec!["15"]);
13060 }
13061
13062 #[test]
13063 fn test_block_body_closure_as_hof_arg() {
13064 let output = run_output(
13065 "let nums = [1, 2, 3]\nlet result = map(nums, (x) -> int64 { let doubled = x * 2\n doubled + 1 })\nprint(result)",
13066 );
13067 assert_eq!(output, vec!["[3, 5, 7]"]);
13068 }
13069
13070 #[test]
13071 fn test_block_body_closure_multi_stmt() {
13072 let output = run_output(
13073 "let f = (a, b) -> int64 { let sum = a + b\n let product = a * b\n sum + product }\nprint(f(3, 4))",
13074 );
13075 assert_eq!(output, vec!["19"]);
13076 }
13077
13078 #[test]
13079 fn test_type_alias_noop() {
13080 let output = run_output(
13082 "type Mapper = fn(int64) -> int64\nlet f: Mapper = (x) => x * 2\nprint(f(5))",
13083 );
13084 assert_eq!(output, vec!["10"]);
13085 }
13086
13087 #[test]
13088 fn test_type_alias_in_function_sig() {
13089 let output = run_output(
13090 "type Mapper = fn(int64) -> int64\nfn apply(f: Mapper, x: int64) -> int64 { f(x) }\nprint(apply((x) => x + 10, 5))",
13091 );
13092 assert_eq!(output, vec!["15"]);
13093 }
13094
13095 #[test]
13096 fn test_shorthand_closure() {
13097 let output = run_output("let double = x => x * 2\nprint(double(5))");
13098 assert_eq!(output, vec!["10"]);
13099 }
13100
13101 #[test]
13102 fn test_shorthand_closure_in_map() {
13103 let output = run_output("let nums = [1, 2, 3]\nprint(map(nums, x => x * 2))");
13104 assert_eq!(output, vec!["[2, 4, 6]"]);
13105 }
13106
13107 #[test]
13108 fn test_iife() {
13109 let output = run_output("let r = ((x) => x * 2)(5)\nprint(r)");
13110 assert_eq!(output, vec!["10"]);
13111 }
13112
13113 #[test]
13114 fn test_hof_apply() {
13115 let output = run_output("fn apply(f, x) { f(x) }\nprint(apply((x) => x + 10, 5))");
13116 assert_eq!(output, vec!["15"]);
13117 }
13118
13119 #[test]
13120 fn test_closure_stored_in_list() {
13121 let output = run_output(
13122 "let fns = [(x) => x + 1, (x) => x * 2]\nprint(fns[0](5))\nprint(fns[1](5))",
13123 );
13124 assert_eq!(output, vec!["6", "10"]);
13125 }
13126
13127 #[test]
13128 fn test_block_body_closure_with_return() {
13129 let output = run_output(
13131 "let classify = (x) -> string { if x > 0 { return \"positive\" }\n \"non-positive\" }\nprint(classify(5))\nprint(classify(-1))",
13132 );
13133 assert_eq!(output, vec!["positive", "non-positive"]);
13134 }
13135
13136 #[test]
13137 fn test_shorthand_closure_in_filter() {
13138 let output = run_output(
13139 "let nums = [1, 2, 3, 4, 5, 6]\nlet evens = filter(nums, x => x % 2 == 0)\nprint(evens)",
13140 );
13141 assert_eq!(output, vec!["[2, 4, 6]"]);
13142 }
13143
13144 #[test]
13145 fn test_block_closure_with_multiple_returns() {
13146 let output = run_output(
13147 "let abs_val = (x) -> int64 { if x < 0 { return -x }\n x }\nprint(abs_val(-5))\nprint(abs_val(3))",
13148 );
13149 assert_eq!(output, vec!["5", "3"]);
13150 }
13151
13152 #[test]
13153 fn test_type_alias_with_block_closure() {
13154 let output = run_output(
13155 "type Transform = fn(int64) -> int64\nlet f: Transform = (x) -> int64 { let y = x * x\n y + 1 }\nprint(f(3))",
13156 );
13157 assert_eq!(output, vec!["10"]);
13158 }
13159
13160 #[test]
13161 fn test_closure_both_backends_expr() {
13162 let output = run_output("let f = (x) => x * 3 + 1\nprint(f(4))");
13164 assert_eq!(output, vec!["13"]);
13165 }
13166
13167 #[test]
13169 #[cfg(not(feature = "python"))]
13170 fn test_py_feature_disabled() {
13171 let result = run("py_import(\"math\")");
13172 assert!(result.is_err());
13173 let msg = format!("{}", result.unwrap_err());
13174 assert!(msg.contains("python") && msg.contains("feature"));
13175 }
13176
13177 #[test]
13178 #[cfg(feature = "python")]
13179 fn test_vm_py_import_and_eval() {
13180 pyo3::prepare_freethreaded_python();
13181 let output = run_output("let m = py_import(\"math\")\nlet pi = m.pi\nprint(pi)");
13182 assert_eq!(output.len(), 1);
13183 let pi: f64 = output[0].parse().unwrap();
13184 assert!((pi - std::f64::consts::PI).abs() < 1e-10);
13185 }
13186
13187 #[test]
13188 #[cfg(feature = "python")]
13189 fn test_vm_py_eval_arithmetic() {
13190 pyo3::prepare_freethreaded_python();
13191 let output = run_output("let x = py_eval(\"2 ** 10\")\nprint(x)");
13192 assert_eq!(output, vec!["1024"]);
13193 }
13194
13195 #[test]
13196 #[cfg(feature = "python")]
13197 fn test_vm_py_method_dispatch() {
13198 pyo3::prepare_freethreaded_python();
13199 let output = run_output("let m = py_import(\"math\")\nprint(m.sqrt(25.0))");
13200 assert_eq!(output, vec!["5.0"]);
13201 }
13202
13203 #[test]
13204 #[cfg(feature = "python")]
13205 fn test_vm_py_list_conversion() {
13206 pyo3::prepare_freethreaded_python();
13207 let output = run_output("let x = py_eval(\"[10, 20, 30]\")\nprint(x)");
13208 assert_eq!(output, vec!["[10, 20, 30]"]);
13209 }
13210
13211 #[test]
13212 #[cfg(feature = "python")]
13213 fn test_vm_py_none_conversion() {
13214 pyo3::prepare_freethreaded_python();
13215 let output = run_output("let x = py_eval(\"None\")\nprint(x)");
13216 assert_eq!(output, vec!["none"]);
13217 }
13218
13219 #[test]
13220 #[cfg(feature = "python")]
13221 fn test_vm_py_error_msg_quality() {
13222 pyo3::prepare_freethreaded_python();
13223 let result = run("py_import(\"nonexistent_xyz_module\")");
13224 assert!(result.is_err());
13225 let msg = format!("{}", result.unwrap_err());
13226 assert!(msg.contains("py_import") && msg.contains("nonexistent_xyz_module"));
13227 }
13228
13229 #[test]
13230 #[cfg(feature = "python")]
13231 fn test_vm_py_getattr_setattr() {
13232 pyo3::prepare_freethreaded_python();
13233 let output = run_output(
13234 "let t = py_import(\"types\")\nlet obj = py_call(py_getattr(t, \"SimpleNamespace\"))\npy_setattr(obj, \"val\", 99)\nprint(py_getattr(obj, \"val\"))",
13235 );
13236 assert_eq!(output, vec!["99"]);
13237 }
13238
13239 #[test]
13240 #[cfg(feature = "python")]
13241 fn test_vm_py_callable_round_trip() {
13242 pyo3::prepare_freethreaded_python();
13243 let output = run_output(
13244 "let m = py_import(\"math\")\nlet f = py_getattr(m, \"floor\")\nprint(py_call(f, 3.7))",
13245 );
13246 assert_eq!(output, vec!["3"]);
13247 }
13248
13249 #[test]
13252 fn test_vm_schema_register_and_get() {
13253 let source = r#"let fields = map_from("id", "int64", "name", "string")
13254schema_register("User", 1, fields)
13255let result = schema_get("User", 1)
13256print(len(result))"#;
13257 let output = run_output(source);
13258 assert_eq!(output, vec!["2"]);
13259 }
13260
13261 #[test]
13262 fn test_vm_schema_latest() {
13263 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13264schema_register("User", 2, map_from("id", "int64", "name", "string"))
13265let latest = schema_latest("User")
13266print(latest)"#;
13267 let output = run_output(source);
13268 assert_eq!(output, vec!["2"]);
13269 }
13270
13271 #[test]
13272 fn test_vm_schema_history() {
13273 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13274schema_register("User", 2, map_from("id", "int64", "name", "string"))
13275let hist = schema_history("User")
13276print(len(hist))"#;
13277 let output = run_output(source);
13278 assert_eq!(output, vec!["2"]);
13279 }
13280
13281 #[test]
13282 fn test_vm_schema_check_backward_compat() {
13283 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13284schema_register("User", 2, map_from("id", "int64", "name", "string"))
13285let issues = schema_check("User", 1, 2, "backward")
13286print(len(issues))"#;
13287 let output = run_output(source);
13288 assert_eq!(output, vec!["0"]);
13289 }
13290
13291 #[test]
13292 fn test_vm_schema_diff() {
13293 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13294schema_register("User", 2, map_from("id", "int64", "name", "string"))
13295let diffs = schema_diff("User", 1, 2)
13296print(len(diffs))"#;
13297 let output = run_output(source);
13298 assert_eq!(output, vec!["1"]);
13299 }
13300
13301 #[test]
13302 fn test_vm_schema_versions() {
13303 let source = r#"schema_register("T", 1, map_from("id", "int64"))
13304schema_register("T", 3, map_from("id", "int64"))
13305schema_register("T", 2, map_from("id", "int64"))
13306let vers = schema_versions("T")
13307print(len(vers))"#;
13308 let output = run_output(source);
13309 assert_eq!(output, vec!["3"]);
13310 }
13311
13312 #[test]
13313 fn test_vm_schema_fields() {
13314 let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13315let fields = schema_fields("User", 1)
13316print(len(fields))"#;
13317 let output = run_output(source);
13318 assert_eq!(output, vec!["2"]);
13319 }
13320
13321 #[test]
13322 fn test_vm_compile_versioned_schema() {
13323 let source = "/// @version 1\nschema User { id: int64, name: string }\nprint(User)";
13324 let output = run_output(source);
13325 assert!(output[0].contains("__schema__:User:v1:"));
13326 }
13327
13328 #[test]
13329 fn test_vm_compile_migrate() {
13330 let source = "migrate User from 1 to 2 { add_column(email: string) }\nprint(\"ok\")";
13331 let output = run_output(source);
13332 assert_eq!(output, vec!["ok"]);
13333 }
13334
13335 #[test]
13336 fn test_vm_schema_check_backward_compat_fails() {
13337 let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13338schema_register("User", 2, map_from("id", "int64"))
13339let issues = schema_check("User", 1, 2, "backward")
13340print(len(issues))"#;
13341 let output = run_output(source);
13342 assert_eq!(output, vec!["1"]);
13343 }
13344
13345 #[test]
13348 fn test_vm_decimal_literal_and_arithmetic() {
13349 let output = run_output("let a = 10.5d\nlet b = 2.5d\nprint(a + b)\nprint(a * b)");
13350 assert_eq!(output, vec!["13.0", "26.25"]);
13351 }
13352
13353 #[test]
13354 fn test_vm_decimal_div_by_zero() {
13355 let source = "let a = 1.0d\nlet b = 0.0d\nlet c = a / b";
13356 let program = tl_parser::parse(source).unwrap();
13357 let proto = crate::compile(&program).unwrap();
13358 let mut vm = Vm::new();
13359 let result = vm.execute(&proto);
13360 assert!(result.is_err());
13361 }
13362
13363 #[test]
13364 fn test_vm_decimal_comparison_ops() {
13365 let output =
13366 run_output("let a = 1.0d\nlet b = 2.0d\nprint(a < b)\nprint(a >= b)\nprint(a == a)");
13367 assert_eq!(output, vec!["true", "false", "true"]);
13368 }
13369
13370 #[test]
13373 fn test_vm_secret_vault_crud() {
13374 let output = run_output(
13375 "secret_set(\"key\", \"value\")\nlet s = secret_get(\"key\")\nprint(s)\nsecret_delete(\"key\")\nlet s2 = secret_get(\"key\")\nprint(type_of(s2))",
13376 );
13377 assert_eq!(output, vec!["***", "none"]);
13378 }
13379
13380 #[test]
13381 fn test_vm_mask_email_basic() {
13382 let output = run_output("print(mask_email(\"alice@domain.com\"))");
13383 assert_eq!(output, vec!["a***@domain.com"]);
13384 }
13385
13386 #[test]
13387 fn test_vm_mask_phone_basic() {
13388 let output = run_output("print(mask_phone(\"123-456-7890\"))");
13389 assert_eq!(output, vec!["***-***-7890"]);
13390 }
13391
13392 #[test]
13393 fn test_vm_mask_cc_basic() {
13394 let output = run_output("print(mask_cc(\"4111222233334444\"))");
13395 assert_eq!(output, vec!["****-****-****-4444"]);
13396 }
13397
13398 #[test]
13399 fn test_vm_hash_produces_hex() {
13400 let output = run_output("let h = hash(\"test\", \"sha256\")\nprint(len(h))");
13401 assert_eq!(output, vec!["64"]);
13402 }
13403
13404 #[test]
13405 fn test_vm_redact_modes() {
13406 let output =
13407 run_output("print(redact(\"hello\", \"full\"))\nprint(redact(\"hello\", \"partial\"))");
13408 assert_eq!(output, vec!["***", "h***o"]);
13409 }
13410
13411 #[test]
13412 fn test_vm_security_policy_sandbox() {
13413 let source = "print(check_permission(\"network\"))\nprint(check_permission(\"file_read\"))";
13414 let program = tl_parser::parse(source).unwrap();
13415 let proto = crate::compile(&program).unwrap();
13416 let mut vm = Vm::new();
13417 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13418 vm.execute(&proto).unwrap();
13419 assert_eq!(vm.output, vec!["false", "true"]);
13420 }
13421
13422 #[cfg(feature = "async-runtime")]
13425 #[test]
13426 fn test_vm_async_read_write_file() {
13427 let dir = tempfile::tempdir().unwrap();
13428 let path = dir.path().join("async_test.txt");
13429 let path_str = path.to_str().unwrap().replace('\\', "/");
13430 let source = format!(
13431 r#"let wt = async_write_file("{path_str}", "async hello")
13432let wr = await(wt)
13433let rt = async_read_file("{path_str}")
13434let content = await(rt)
13435print(content)"#
13436 );
13437 let output = run_output(&source);
13438 assert_eq!(output, vec!["async hello"]);
13439 }
13440
13441 #[cfg(feature = "async-runtime")]
13442 #[test]
13443 fn test_vm_async_sleep() {
13444 let source = r#"
13445let t = async_sleep(10)
13446let r = await(t)
13447print(r)
13448"#;
13449 let output = run_output(source);
13450 assert_eq!(output, vec!["none"]);
13451 }
13452
13453 #[cfg(feature = "async-runtime")]
13454 #[test]
13455 fn test_vm_select_first_wins() {
13456 let source = r#"
13458let fast = async_sleep(10)
13459let slow = async_sleep(5000)
13460let winner = select(fast, slow)
13461let result = await(winner)
13462print(result)
13463"#;
13464 let output = run_output(source);
13465 assert_eq!(output, vec!["none"]);
13466 }
13467
13468 #[cfg(feature = "async-runtime")]
13469 #[test]
13470 fn test_vm_race_all() {
13471 let source = r#"
13472let t1 = async_sleep(10)
13473let t2 = async_sleep(5000)
13474let winner = race_all([t1, t2])
13475let result = await(winner)
13476print(result)
13477"#;
13478 let output = run_output(source);
13479 assert_eq!(output, vec!["none"]);
13480 }
13481
13482 #[cfg(feature = "async-runtime")]
13483 #[test]
13484 fn test_vm_async_map() {
13485 let source = r#"
13486let items = [1, 2, 3]
13487let t = async_map(items, (x) => x * 10)
13488let result = await(t)
13489print(result)
13490"#;
13491 let output = run_output(source);
13492 assert_eq!(output, vec!["[10, 20, 30]"]);
13493 }
13494
13495 #[cfg(feature = "async-runtime")]
13496 #[test]
13497 fn test_vm_async_filter() {
13498 let source = r#"
13499let items = [1, 2, 3, 4, 5]
13500let t = async_filter(items, (x) => x > 3)
13501let result = await(t)
13502print(result)
13503"#;
13504 let output = run_output(source);
13505 assert_eq!(output, vec!["[4, 5]"]);
13506 }
13507
13508 #[cfg(feature = "async-runtime")]
13509 #[test]
13510 fn test_vm_async_write_file_returns_none() {
13511 let dir = tempfile::tempdir().unwrap();
13512 let path = dir.path().join("write_test.txt");
13513 let path_str = path.to_str().unwrap().replace('\\', "/");
13514 let source = format!(
13515 r#"let t = async_write_file("{path_str}", "test data")
13516let r = await(t)
13517print(r)"#
13518 );
13519 let output = run_output(&source);
13520 assert_eq!(output, vec!["none"]);
13521 }
13522
13523 #[cfg(feature = "async-runtime")]
13524 #[test]
13525 fn test_vm_async_security_policy_blocks_write() {
13526 let source = r#"let t = async_write_file("/tmp/blocked.txt", "data")"#;
13527 let program = tl_parser::parse(source).unwrap();
13528 let proto = crate::compile(&program).unwrap();
13529 let mut vm = Vm::new();
13530 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13531 let result = vm.execute(&proto);
13532 assert!(result.is_err());
13533 let err = format!("{}", result.unwrap_err());
13534 assert!(
13535 err.contains("file_write not allowed"),
13536 "Expected security error, got: {err}"
13537 );
13538 }
13539
13540 #[cfg(feature = "async-runtime")]
13541 #[test]
13542 fn test_vm_async_security_policy_allows_read() {
13543 let dir = tempfile::tempdir().unwrap();
13545 let path = dir.path().join("readable.txt");
13546 std::fs::write(&path, "safe content").unwrap();
13547 let path_str = path.to_str().unwrap().replace('\\', "/");
13548 let source = format!(
13549 r#"let t = async_read_file("{path_str}")
13550let r = await(t)
13551print(r)"#
13552 );
13553 let program = tl_parser::parse(&source).unwrap();
13554 let proto = crate::compile(&program).unwrap();
13555 let mut vm = Vm::new();
13556 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13557 vm.execute(&proto).unwrap();
13558 assert_eq!(vm.output, vec!["safe content"]);
13559 }
13560
13561 #[cfg(feature = "async-runtime")]
13562 #[test]
13563 fn test_vm_async_map_empty_list() {
13564 let source = r#"
13565let t = async_map([], (x) => x * 2)
13566let result = await(t)
13567print(result)
13568"#;
13569 let output = run_output(source);
13570 assert_eq!(output, vec!["[]"]);
13571 }
13572
13573 #[cfg(feature = "async-runtime")]
13574 #[test]
13575 fn test_vm_async_filter_none_match() {
13576 let source = r#"
13577let t = async_filter([1, 2, 3], (x) => x > 100)
13578let result = await(t)
13579print(result)
13580"#;
13581 let output = run_output(source);
13582 assert_eq!(output, vec!["[]"]);
13583 }
13584
13585 #[test]
13588 fn test_vm_closure_returned_from_function() {
13589 let output = run_output(
13590 r#"
13591fn make_adder(n) {
13592 return (x) => x + n
13593}
13594let add5 = make_adder(5)
13595print(add5(3))
13596print(add5(10))
13597"#,
13598 );
13599 assert_eq!(output, vec!["8", "15"]);
13600 }
13601
13602 #[test]
13603 fn test_vm_closure_factory_multiple_calls() {
13604 let output = run_output(
13605 r#"
13606fn make_adder(n) {
13607 return (x) => x + n
13608}
13609let add2 = make_adder(2)
13610let add10 = make_adder(10)
13611print(add2(5))
13612print(add10(5))
13613print(add2(1))
13614"#,
13615 );
13616 assert_eq!(output, vec!["7", "15", "3"]);
13617 }
13618
13619 #[test]
13620 fn test_vm_closure_returned_in_list() {
13621 let output = run_output(
13622 r#"
13623fn make_ops(n) {
13624 let add = (x) => x + n
13625 let mul = (x) => x * n
13626 return [add, mul]
13627}
13628let ops = make_ops(3)
13629print(ops[0](10))
13630print(ops[1](10))
13631"#,
13632 );
13633 assert_eq!(output, vec!["13", "30"]);
13634 }
13635
13636 #[test]
13637 fn test_vm_nested_closure_return() {
13638 let output = run_output(
13639 r#"
13640fn outer(a) {
13641 fn inner(b) {
13642 return (x) => x + a + b
13643 }
13644 return inner(10)
13645}
13646let f = outer(5)
13647print(f(1))
13648"#,
13649 );
13650 assert_eq!(output, vec!["16"]);
13651 }
13652
13653 #[test]
13654 fn test_vm_multiple_closures_same_local() {
13655 let output = run_output(
13656 r#"
13657fn make_pair(n) {
13658 let inc = (x) => x + n
13659 let dec = (x) => x - n
13660 return [inc, dec]
13661}
13662let pair = make_pair(7)
13663print(pair[0](10))
13664print(pair[1](10))
13665"#,
13666 );
13667 assert_eq!(output, vec!["17", "3"]);
13668 }
13669
13670 #[test]
13671 fn test_vm_closure_captures_multiple_locals() {
13672 let output = run_output(
13673 r#"
13674fn make_greeter(greeting, name) {
13675 let sep = " "
13676 return () => greeting + sep + name
13677}
13678let hi = make_greeter("Hello", "World")
13679let bye = make_greeter("Goodbye", "Alice")
13680print(hi())
13681print(bye())
13682"#,
13683 );
13684 assert_eq!(output, vec!["Hello World", "Goodbye Alice"]);
13685 }
13686
13687 #[test]
13690 fn test_vm_throw_catch_preserves_enum() {
13691 let output = run_output(
13692 r#"
13693enum Color { Red, Green(x) }
13694try {
13695 throw Color::Green(42)
13696} catch e {
13697 match e {
13698 Color::Green(x) => print(x),
13699 _ => print("no match"),
13700 }
13701}
13702"#,
13703 );
13704 assert_eq!(output, vec!["42"]);
13705 }
13706
13707 #[test]
13708 fn test_vm_throw_catch_string_compat() {
13709 let output = run_output(
13710 r#"
13711try {
13712 throw "hello error"
13713} catch e {
13714 print(e)
13715}
13716"#,
13717 );
13718 assert_eq!(output, vec!["hello error"]);
13719 }
13720
13721 #[test]
13722 fn test_vm_runtime_error_still_string() {
13723 let output = run_output(
13724 r#"
13725try {
13726 let x = 1 / 0
13727} catch e {
13728 print(type_of(e))
13729}
13730"#,
13731 );
13732 assert_eq!(output, vec!["string"]);
13733 }
13734
13735 #[test]
13736 fn test_vm_data_error_construct_and_throw() {
13737 let output = run_output(
13738 r#"
13739try {
13740 throw DataError::ParseError("bad format", "file.csv")
13741} catch e {
13742 print(match e { DataError::ParseError(msg, _) => msg, _ => "no match" })
13743 print(match e { DataError::ParseError(_, src) => src, _ => "no match" })
13744}
13745"#,
13746 );
13747 assert_eq!(output, vec!["bad format", "file.csv"]);
13748 }
13749
13750 #[test]
13751 fn test_vm_network_error_construct() {
13752 let output = run_output(
13753 r#"
13754let err = NetworkError::TimeoutError("timed out")
13755match err {
13756 NetworkError::TimeoutError(msg) => print(msg),
13757 _ => print("no match"),
13758}
13759"#,
13760 );
13761 assert_eq!(output, vec!["timed out"]);
13762 }
13763
13764 #[test]
13765 fn test_vm_connector_error_construct() {
13766 let output = run_output(
13767 r#"
13768let err = ConnectorError::AuthError("invalid creds", "postgres")
13769print(match err { ConnectorError::AuthError(msg, _) => msg, _ => "no match" })
13770print(match err { ConnectorError::AuthError(_, conn) => conn, _ => "no match" })
13771"#,
13772 );
13773 assert_eq!(output, vec!["invalid creds", "postgres"]);
13774 }
13775
13776 #[test]
13777 fn test_vm_is_error_builtin() {
13778 let output = run_output(
13779 r#"
13780let e1 = DataError::NotFound("users")
13781let e2 = NetworkError::TimeoutError("slow")
13782let e3 = ConnectorError::ConfigError("bad", "redis")
13783let e4 = "not an error"
13784print(is_error(e1))
13785print(is_error(e2))
13786print(is_error(e3))
13787print(is_error(e4))
13788"#,
13789 );
13790 assert_eq!(output, vec!["true", "true", "true", "false"]);
13791 }
13792
13793 #[test]
13794 fn test_vm_error_type_builtin() {
13795 let output = run_output(
13796 r#"
13797let e1 = DataError::ParseError("bad", "x.csv")
13798let e2 = NetworkError::HttpError("fail", "url")
13799let e3 = "not an error"
13800print(error_type(e1))
13801print(error_type(e2))
13802print(error_type(e3))
13803"#,
13804 );
13805 assert_eq!(output, vec!["DataError", "NetworkError", "none"]);
13806 }
13807
13808 #[test]
13809 fn test_vm_match_error_variants() {
13810 let output = run_output(
13811 r#"
13812fn handle(err) {
13813 print(match err {
13814 DataError::ParseError(msg, _) => "parse: " + msg,
13815 DataError::SchemaError(msg, _, _) => "schema: " + msg,
13816 DataError::ValidationError(_, field) => "validation: " + field,
13817 DataError::NotFound(name) => "not found: " + name,
13818 _ => "unknown"
13819 })
13820}
13821handle(DataError::ParseError("bad csv", "data.csv"))
13822handle(DataError::NotFound("users_table"))
13823handle(DataError::SchemaError("mismatch", "int", "string"))
13824handle(DataError::ValidationError("invalid", "email"))
13825"#,
13826 );
13827 assert_eq!(
13828 output,
13829 vec![
13830 "parse: bad csv",
13831 "not found: users_table",
13832 "schema: mismatch",
13833 "validation: email",
13834 ]
13835 );
13836 }
13837
13838 #[test]
13839 fn test_vm_rethrow_structured_error() {
13840 let output = run_output(
13841 r#"
13842try {
13843 try {
13844 throw DataError::NotFound("config")
13845 } catch e {
13846 throw e
13847 }
13848} catch outer {
13849 match outer {
13850 DataError::NotFound(name) => print("caught: " + name),
13851 _ => print("wrong type"),
13852 }
13853}
13854"#,
13855 );
13856 assert_eq!(output, vec!["caught: config"]);
13857 }
13858
13859 #[test]
13862 fn test_vm_pipe_moves_value() {
13863 let result = run(r#"
13865fn identity(v) { v }
13866let x = [1, 2, 3]
13867x |> identity()
13868print(x)
13869"#);
13870 assert!(result.is_err());
13871 let err = result.unwrap_err().to_string();
13872 assert!(err.contains("moved"), "Error should mention 'moved': {err}");
13873 }
13874
13875 #[test]
13876 fn test_vm_clone_before_pipe() {
13877 let output = run_output(
13879 r#"
13880fn identity(v) { v }
13881let x = [1, 2, 3]
13882x.clone() |> identity()
13883print(x)
13884"#,
13885 );
13886 assert_eq!(output, vec!["[1, 2, 3]"]);
13887 }
13888
13889 #[test]
13890 fn test_vm_clone_list_deep() {
13891 let output = run_output(
13893 r#"
13894let original = [1, 2, 3]
13895let copy = original.clone()
13896copy[0] = 99
13897print(original)
13898print(copy)
13899"#,
13900 );
13901 assert_eq!(output, vec!["[1, 2, 3]", "[99, 2, 3]"]);
13902 }
13903
13904 #[test]
13905 fn test_vm_clone_map() {
13906 let output = run_output(
13907 r#"
13908let m = map_from("a", 1, "b", 2)
13909let m2 = m.clone()
13910m2["a"] = 99
13911print(m)
13912print(m2)
13913"#,
13914 );
13915 assert_eq!(output, vec!["{a: 1, b: 2}", "{a: 99, b: 2}"]);
13916 }
13917
13918 #[test]
13919 fn test_vm_clone_struct() {
13920 let output = run_output(
13921 r#"
13922struct Point { x: int64, y: int64 }
13923let p = Point { x: 1, y: 2 }
13924let p2 = p.clone()
13925print(p)
13926print(p2)
13927"#,
13928 );
13929 assert_eq!(output, vec!["Point { x: 1, y: 2 }", "Point { x: 1, y: 2 }"]);
13930 }
13931
13932 #[test]
13933 fn test_vm_ref_read_only() {
13934 let result = run(r#"
13936let x = [1, 2, 3]
13937let r = &x
13938r[0] = 99
13939"#);
13940 assert!(result.is_err());
13941 let err = result.unwrap_err().to_string();
13942 assert!(
13943 err.contains("Cannot mutate a borrowed reference"),
13944 "Error should mention reference: {err}"
13945 );
13946 }
13947
13948 #[test]
13949 fn test_vm_ref_transparent_read() {
13950 let output = run_output(
13952 r#"
13953let x = [1, 2, 3]
13954let r = &x
13955print(len(r))
13956"#,
13957 );
13958 assert_eq!(output, vec!["3"]);
13959 }
13960
13961 #[test]
13962 fn test_vm_parallel_for_basic() {
13963 let output = run_output(
13965 r#"
13966let items = [10, 20, 30]
13967parallel for item in items {
13968 print(item)
13969}
13970"#,
13971 );
13972 assert_eq!(output, vec!["10", "20", "30"]);
13973 }
13974
13975 #[test]
13976 fn test_vm_moved_value_clear_error() {
13977 let result = run(r#"
13979fn f(x) { x }
13980let data = "hello"
13981data |> f()
13982print(data)
13983"#);
13984 assert!(result.is_err());
13985 let err = result.unwrap_err().to_string();
13986 assert!(
13987 err.contains("clone()"),
13988 "Error should suggest .clone(): {err}"
13989 );
13990 }
13991
13992 #[test]
13993 fn test_vm_reassign_after_move() {
13994 let output = run_output(
13996 r#"
13997fn f(x) { x }
13998let x = 1
13999x |> f()
14000let x = 2
14001print(x)
14002"#,
14003 );
14004 assert_eq!(output, vec!["2"]);
14005 }
14006
14007 #[test]
14008 fn test_vm_pipe_chain_move() {
14009 let output = run_output(
14011 r#"
14012fn double(x) { x * 2 }
14013fn add_one(x) { x + 1 }
14014let result = 5 |> double() |> add_one()
14015print(result)
14016"#,
14017 );
14018 assert_eq!(output, vec!["11"]);
14019 }
14020
14021 #[test]
14022 fn test_vm_string_clone() {
14023 let output = run_output(
14025 r#"
14026let s = "hello"
14027let s2 = s.clone()
14028print(s)
14029print(s2)
14030"#,
14031 );
14032 assert_eq!(output, vec!["hello", "hello"]);
14033 }
14034
14035 #[test]
14036 fn test_vm_ref_method_dispatch() {
14037 let output = run_output(
14039 r#"
14040let s = "hello world"
14041let r = &s
14042print(r.len())
14043"#,
14044 );
14045 assert_eq!(output, vec!["11"]);
14046 }
14047
14048 #[test]
14049 fn test_vm_ref_member_access() {
14050 let output = run_output(
14052 r#"
14053struct Point { x: int64, y: int64 }
14054let p = Point { x: 10, y: 20 }
14055let r = &p
14056print(r.x)
14057"#,
14058 );
14059 assert_eq!(output, vec!["10"]);
14060 }
14061
14062 #[test]
14063 fn test_vm_ref_set_member_blocked() {
14064 let result = run(r#"
14066struct Point { x: int64, y: int64 }
14067let p = Point { x: 10, y: 20 }
14068let r = &p
14069r.x = 99
14070"#);
14071 assert!(result.is_err());
14072 let err = result.unwrap_err().to_string();
14073 assert!(
14074 err.contains("Cannot mutate a borrowed reference"),
14075 "Error: {err}"
14076 );
14077 }
14078
14079 #[test]
14082 fn test_ir_filter_merge_chain() {
14083 let dir = tempfile::tempdir().unwrap();
14085 let csv = dir.path().join("data.csv");
14086 std::fs::write(&csv, "name,age\nAlice,30\nBob,20\nCharlie,35\n").unwrap();
14087 let src = format!(
14088 r#"let t = read_csv("{}")
14089let r = t |> filter(age > 25) |> filter(age < 40) |> collect()
14090print(r)"#,
14091 csv.to_str().unwrap()
14092 );
14093 let output = run_output(&src);
14094 assert!(
14096 output[0].contains("Alice"),
14097 "Output should contain Alice: {}",
14098 output[0]
14099 );
14100 assert!(
14101 output[0].contains("Charlie"),
14102 "Output should contain Charlie: {}",
14103 output[0]
14104 );
14105 assert!(
14106 !output[0].contains("Bob"),
14107 "Output should not contain Bob: {}",
14108 output[0]
14109 );
14110 }
14111
14112 #[test]
14113 fn test_ir_predicate_pushdown_through_select() {
14114 let dir = tempfile::tempdir().unwrap();
14116 let csv = dir.path().join("data.csv");
14117 std::fs::write(
14118 &csv,
14119 "name,age,city\nAlice,30,NYC\nBob,20,LA\nCharlie,35,NYC\n",
14120 )
14121 .unwrap();
14122 let src = format!(
14123 r#"let t = read_csv("{}")
14124let r = t |> select(name, age) |> filter(age > 25) |> collect()
14125print(r)"#,
14126 csv.to_str().unwrap()
14127 );
14128 let output = run_output(&src);
14129 assert!(output[0].contains("Alice"), "Output should contain Alice");
14130 assert!(
14131 output[0].contains("Charlie"),
14132 "Output should contain Charlie"
14133 );
14134 assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14135 }
14136
14137 #[test]
14138 fn test_ir_sort_filter_pushdown() {
14139 let dir = tempfile::tempdir().unwrap();
14141 let csv = dir.path().join("data.csv");
14142 std::fs::write(&csv, "name,score\nAlice,90\nBob,50\nCharlie,75\n").unwrap();
14143 let src = format!(
14144 r#"let t = read_csv("{}")
14145let r = t |> sort(score, "desc") |> filter(score > 60) |> collect()
14146print(r)"#,
14147 csv.to_str().unwrap()
14148 );
14149 let output = run_output(&src);
14150 assert!(output[0].contains("Alice"), "Output should contain Alice");
14151 assert!(
14152 output[0].contains("Charlie"),
14153 "Output should contain Charlie"
14154 );
14155 assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14156 }
14157
14158 #[test]
14159 fn test_ir_multi_operation_chain() {
14160 let dir = tempfile::tempdir().unwrap();
14162 let csv = dir.path().join("data.csv");
14163 std::fs::write(
14164 &csv,
14165 "name,age,dept\nAlice,30,Eng\nBob,20,Sales\nCharlie,35,Eng\nDiana,28,Sales\n",
14166 )
14167 .unwrap();
14168 let src = format!(
14169 r#"let t = read_csv("{}")
14170let r = t |> filter(age > 22) |> select(name, age) |> sort(age, "desc") |> limit(2) |> collect()
14171print(r)"#,
14172 csv.to_str().unwrap()
14173 );
14174 let output = run_output(&src);
14175 assert!(output[0].contains("Charlie"), "Output: {}", output[0]);
14177 assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14178 }
14179
14180 #[test]
14181 fn test_ir_pipe_move_semantics_preserved() {
14182 let dir = tempfile::tempdir().unwrap();
14184 let csv = dir.path().join("data.csv");
14185 std::fs::write(&csv, "name,age\nAlice,30\n").unwrap();
14186 let src = format!(
14187 r#"let t = read_csv("{}")
14188let r = t |> filter(age > 0) |> collect()
14189print(t)"#,
14190 csv.to_str().unwrap()
14191 );
14192 let result = run(&src);
14193 assert!(result.is_err(), "Should error on use-after-move");
14194 }
14195
14196 #[test]
14197 fn test_ir_non_table_op_fallback() {
14198 let output = run_output(
14200 r#"
14201fn double(x) { x * 2 }
14202let result = 5 |> double()
14203print(result)
14204"#,
14205 );
14206 assert_eq!(output, vec!["10"]);
14207 }
14208
14209 #[test]
14210 fn test_ir_mixed_pipe_fallback() {
14211 let output = run_output(
14213 r#"
14214let result = [3, 1, 2] |> len()
14215print(result)
14216"#,
14217 );
14218 assert_eq!(output, vec!["3"]);
14219 }
14220
14221 #[test]
14222 fn test_ir_single_filter_roundtrip() {
14223 let dir = tempfile::tempdir().unwrap();
14225 let csv = dir.path().join("data.csv");
14226 std::fs::write(&csv, "name,age\nAlice,30\nBob,20\n").unwrap();
14227 let src = format!(
14228 r#"let t = read_csv("{}")
14229let r = t |> filter(age > 25) |> collect()
14230print(r)"#,
14231 csv.to_str().unwrap()
14232 );
14233 let output = run_output(&src);
14234 assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14235 assert!(!output[0].contains("Bob"), "Output: {}", output[0]);
14236 }
14237
14238 #[test]
14241 fn test_vm_agent_definition() {
14242 let output = run_output(
14243 r#"
14244fn search(query) { "found: " + query }
14245agent bot {
14246 model: "gpt-4o",
14247 system: "You are helpful.",
14248 tools {
14249 search: {
14250 description: "Search the web",
14251 parameters: {}
14252 }
14253 },
14254 max_turns: 5
14255}
14256print(type_of(bot))
14257print(bot)
14258"#,
14259 );
14260 assert_eq!(output, vec!["agent", "<agent bot>"]);
14261 }
14262
14263 #[test]
14264 fn test_vm_agent_minimal() {
14265 let output = run_output(
14266 r#"
14267agent minimal_bot {
14268 model: "claude-sonnet-4-20250514"
14269}
14270print(type_of(minimal_bot))
14271"#,
14272 );
14273 assert_eq!(output, vec!["agent"]);
14274 }
14275
14276 #[test]
14277 fn test_vm_agent_with_base_url() {
14278 let output = run_output(
14279 r#"
14280agent local_bot {
14281 model: "llama3",
14282 base_url: "http://localhost:11434/v1",
14283 max_turns: 3
14284}
14285print(local_bot)
14286"#,
14287 );
14288 assert_eq!(output, vec!["<agent local_bot>"]);
14289 }
14290
14291 #[test]
14292 fn test_vm_agent_multiple_tools() {
14293 let output = run_output(
14294 r#"
14295fn search(query) { "result" }
14296fn weather(city) { "sunny" }
14297agent helper {
14298 model: "gpt-4o",
14299 tools {
14300 search: { description: "Search", parameters: {} },
14301 weather: { description: "Get weather", parameters: {} }
14302 }
14303}
14304print(type_of(helper))
14305"#,
14306 );
14307 assert_eq!(output, vec!["agent"]);
14308 }
14309
14310 #[test]
14311 fn test_vm_agent_lifecycle_hooks_stored() {
14312 let output = run_output(
14313 r#"
14314fn search(q) { "result" }
14315agent bot {
14316 model: "gpt-4o",
14317 tools {
14318 search: { description: "Search", parameters: {} }
14319 },
14320 on_tool_call {
14321 println("tool: " + tool_name)
14322 }
14323 on_complete {
14324 println("done")
14325 }
14326}
14327print(type_of(bot))
14328print(type_of(__agent_bot_on_tool_call__))
14329print(type_of(__agent_bot_on_complete__))
14330"#,
14331 );
14332 assert_eq!(output, vec!["agent", "function", "function"]);
14333 }
14334
14335 #[test]
14336 fn test_vm_agent_lifecycle_hook_callable() {
14337 let output = run_output(
14338 r#"
14339agent bot {
14340 model: "gpt-4o",
14341 on_tool_call {
14342 println("called: " + tool_name + " -> " + tool_result)
14343 }
14344 on_complete {
14345 println("completed")
14346 }
14347}
14348__agent_bot_on_tool_call__("search", "query", "found it")
14349__agent_bot_on_complete__("hello")
14350"#,
14351 );
14352 assert_eq!(output, vec!["called: search -> found it", "completed"]);
14353 }
14354}