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) if idx < items.len() => {
1334 let item = items[idx].clone();
1335 self.stack[base + c as usize] = item;
1336 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1337 false
1338 }
1339 VmValue::Map(pairs) if idx < pairs.len() => {
1340 let (k, v) = &pairs[idx];
1341 let pair =
1342 VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]));
1343 self.stack[base + c as usize] = pair;
1344 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1345 false
1346 }
1347 VmValue::Set(items) if idx < items.len() => {
1348 let item = items[idx].clone();
1349 self.stack[base + c as usize] = item;
1350 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1351 false
1352 }
1353 VmValue::Generator(gen_arc) => {
1354 let g = gen_arc.clone();
1355 let val = self.generator_next(&g)?;
1356 if matches!(val, VmValue::None) {
1357 true
1358 } else {
1359 self.stack[base + c as usize] = val;
1360 false
1361 }
1362 }
1363 _ => true,
1364 };
1365 if done {
1366 } else {
1369 self.frames[frame_idx].ip += 1;
1371 }
1372 }
1373 Op::ForPrep => {
1374 }
1376 Op::TestMatch => {
1377 let subject = &self.stack[base + a as usize];
1379 let pattern = &self.stack[base + b as usize];
1380 let matched = match (subject, pattern) {
1381 (VmValue::Int(a), VmValue::Int(b)) => a == b,
1382 (VmValue::Float(a), VmValue::Float(b)) => a == b,
1383 (VmValue::String(a), VmValue::String(b)) => a == b,
1384 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
1385 (VmValue::None, VmValue::None) => true,
1386 (VmValue::EnumInstance(subj), VmValue::EnumInstance(pat)) => {
1388 subj.type_name == pat.type_name && subj.variant == pat.variant
1389 }
1390 (VmValue::StructInstance(s), VmValue::String(name)) => {
1392 s.type_name.as_ref() == name.as_ref()
1393 }
1394 _ => false,
1395 };
1396 self.stack[base + c as usize] = VmValue::Bool(matched);
1397 }
1398 Op::NullCoalesce => {
1399 if matches!(self.stack[base + a as usize], VmValue::None) {
1400 let val = self.stack[base + b as usize].clone();
1401 self.stack[base + a as usize] = val;
1402 }
1403 }
1404 Op::GetMember => {
1405 let field_name = self.get_string_constant(frame_idx, c as u16)?;
1407 let raw_obj = self.stack[base + b as usize].clone();
1408 let obj = match &raw_obj {
1409 VmValue::Ref(inner) => inner.as_ref().clone(),
1410 _ => raw_obj,
1411 };
1412 let result = match &obj {
1413 VmValue::StructInstance(inst) => inst
1414 .fields
1415 .iter()
1416 .find(|(k, _)| k.as_ref() == field_name.as_ref())
1417 .map(|(_, v)| v.clone())
1418 .unwrap_or(VmValue::None),
1419 VmValue::Module(m) => m
1420 .exports
1421 .get(field_name.as_ref())
1422 .cloned()
1423 .unwrap_or(VmValue::None),
1424 VmValue::EnumInstance(e) => match field_name.as_ref() {
1425 "variant" => VmValue::String(e.variant.clone()),
1426 "type_name" => VmValue::String(e.type_name.clone()),
1427 _ => VmValue::None,
1428 },
1429 VmValue::Map(pairs) => pairs
1430 .iter()
1431 .find(|(k, _)| k.as_ref() == field_name.as_ref())
1432 .map(|(_, v)| v.clone())
1433 .unwrap_or(VmValue::None),
1434 #[cfg(feature = "python")]
1435 VmValue::PyObject(wrapper) => {
1436 crate::python::py_get_member(wrapper, field_name.as_ref())
1437 }
1438 _ => VmValue::None,
1439 };
1440 self.stack[base + a as usize] = result;
1441 }
1442 Op::Interpolate => {
1443 let template = self.get_string_constant(frame_idx, bx)?;
1445 let result = self.interpolate_string(&template, base)?;
1446 self.stack[base + a as usize] = VmValue::String(Arc::from(result.as_str()));
1447 }
1448 Op::Train => {
1449 #[cfg(feature = "native")]
1450 {
1451 let result = self.handle_train(frame_idx, b, c)?;
1452 self.stack[base + a as usize] = result;
1453 }
1454 #[cfg(not(feature = "native"))]
1455 {
1456 let _ = (a, b, c, frame_idx);
1457 return Err(runtime_err("AI training not available in WASM"));
1458 }
1459 }
1460 Op::PipelineExec => {
1461 #[cfg(feature = "native")]
1462 {
1463 let result = self.handle_pipeline_exec(frame_idx, b, c)?;
1464 self.stack[base + a as usize] = result;
1465 }
1466 #[cfg(not(feature = "native"))]
1467 {
1468 let _ = (a, b, c, frame_idx);
1469 return Err(runtime_err("Pipelines not available in WASM"));
1470 }
1471 }
1472 Op::StreamExec => {
1473 #[cfg(feature = "native")]
1474 {
1475 let result = self.handle_stream_exec(frame_idx, b)?;
1476 self.stack[base + a as usize] = result;
1477 }
1478 #[cfg(not(feature = "native"))]
1479 {
1480 let _ = (a, b, frame_idx);
1481 return Err(runtime_err("Streaming not available in WASM"));
1482 }
1483 }
1484 Op::ConnectorDecl => {
1485 #[cfg(feature = "native")]
1486 {
1487 let result = self.handle_connector_decl(frame_idx, b, c)?;
1488 self.stack[base + a as usize] = result;
1489 }
1490 #[cfg(not(feature = "native"))]
1491 {
1492 let _ = (a, b, c, frame_idx);
1493 return Err(runtime_err("Connectors not available in WASM"));
1494 }
1495 }
1496
1497 Op::NewStruct => {
1499 let name = self.get_string_constant(frame_idx, b as u16)?;
1506
1507 let is_decl = (c & 0x80) != 0;
1511
1512 if is_decl {
1513 let const_idx = (c & 0x7F) as usize;
1514 let fields_data = match &self.frames[frame_idx].prototype.constants[const_idx] {
1516 Constant::AstExprList(exprs) => exprs.clone(),
1517 _ => Vec::new(),
1518 };
1519 let is_enum = fields_data
1521 .first()
1522 .map(|e| {
1523 if let AstExpr::String(s) = e {
1524 s.contains(':')
1525 } else {
1526 false
1527 }
1528 })
1529 .unwrap_or(false);
1530
1531 if is_enum {
1532 let variants: Vec<(Arc<str>, usize)> = fields_data
1533 .iter()
1534 .filter_map(|e| {
1535 if let AstExpr::String(s) = e {
1536 let parts: Vec<&str> = s.splitn(2, ':').collect();
1537 if parts.len() == 2 {
1538 Some((
1539 Arc::from(parts[0]),
1540 parts[1].parse::<usize>().unwrap_or(0),
1541 ))
1542 } else {
1543 None
1544 }
1545 } else {
1546 None
1547 }
1548 })
1549 .collect();
1550 self.stack[base + a as usize] = VmValue::EnumDef(Arc::new(VmEnumDef {
1551 name: name.clone(),
1552 variants,
1553 }));
1554 } else {
1555 let field_names: Vec<Arc<str>> = fields_data
1556 .iter()
1557 .filter_map(|e| {
1558 if let AstExpr::String(s) = e {
1559 Some(Arc::from(s.as_str()))
1560 } else {
1561 None
1562 }
1563 })
1564 .collect();
1565 self.stack[base + a as usize] = VmValue::StructDef(Arc::new(VmStructDef {
1566 name: name.clone(),
1567 fields: field_names,
1568 }));
1569 }
1570 } else {
1571 let field_count = c as usize;
1573 let next_ip = self.frames[frame_idx].ip;
1575 let next = self.frames[frame_idx]
1576 .prototype
1577 .code
1578 .get(next_ip)
1579 .copied()
1580 .unwrap_or(0);
1581 let start_reg = decode_a(next) as usize;
1582 self.frames[frame_idx].ip += 1; let mut fields = Vec::new();
1585 for i in 0..field_count {
1586 let fname = self.stack[base + start_reg + i * 2].clone();
1587 let fval = self.stack[base + start_reg + i * 2 + 1].clone();
1588 let fname_str = match fname {
1589 VmValue::String(s) => s,
1590 _ => Arc::from(format!("field_{i}").as_str()),
1591 };
1592 fields.push((fname_str, fval));
1593 }
1594 self.stack[base + a as usize] =
1595 VmValue::StructInstance(Arc::new(VmStructInstance {
1596 type_name: name.clone(),
1597 fields,
1598 }));
1599 }
1600 }
1601
1602 Op::SetMember => {
1603 if matches!(&self.stack[base + a as usize], VmValue::Ref(_)) {
1604 return Err(runtime_err(
1605 "Cannot mutate a borrowed reference".to_string(),
1606 ));
1607 }
1608 let field_name = self.get_string_constant(frame_idx, b as u16)?;
1610 let val = self.stack[base + c as usize].clone();
1611 let obj = self.stack[base + a as usize].clone();
1612 if let VmValue::StructInstance(inst) = obj {
1613 let mut new_fields = inst.fields.clone();
1614 let mut found = false;
1615 for (k, v) in &mut new_fields {
1616 if k.as_ref() == field_name.as_ref() {
1617 *v = val.clone();
1618 found = true;
1619 break;
1620 }
1621 }
1622 if !found {
1623 new_fields.push((field_name, val));
1624 }
1625 self.stack[base + a as usize] =
1626 VmValue::StructInstance(Arc::new(VmStructInstance {
1627 type_name: inst.type_name.clone(),
1628 fields: new_fields,
1629 }));
1630 }
1631 }
1632
1633 Op::NewEnum => {
1634 let full_name = self.get_string_constant(frame_idx, b as u16)?;
1637 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1638 self.frames[frame_idx].ip += 1;
1639 let arg_count = decode_a(next) as usize;
1640 let args_start = c as usize;
1641
1642 let parts: Vec<&str> = full_name.splitn(2, "::").collect();
1644 let (type_name, variant) = if parts.len() == 2 {
1645 (Arc::from(parts[0]), Arc::from(parts[1]))
1646 } else {
1647 (Arc::from(""), Arc::from(full_name.as_ref()))
1648 };
1649
1650 let mut fields = Vec::new();
1651 for i in 0..arg_count {
1652 fields.push(self.stack[base + args_start + i].clone());
1653 }
1654
1655 self.stack[base + a as usize] = VmValue::EnumInstance(Arc::new(VmEnumInstance {
1656 type_name,
1657 variant,
1658 fields,
1659 }));
1660 }
1661
1662 Op::MatchEnum => {
1663 let variant_name = self.get_string_constant(frame_idx, b as u16)?;
1665 let subject = &self.stack[base + a as usize];
1666 let matched = match subject {
1667 VmValue::EnumInstance(e) => e.variant.as_ref() == variant_name.as_ref(),
1668 _ => false,
1669 };
1670 self.stack[base + c as usize] = VmValue::Bool(matched);
1671 }
1672
1673 Op::MethodCall => {
1674 let method_name = self.get_string_constant(frame_idx, c as u16)?;
1677 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1678 self.frames[frame_idx].ip += 1;
1679 let args_start = decode_a(next) as usize;
1680 let arg_count = decode_b(next) as usize;
1681
1682 let obj = self.stack[base + b as usize].clone();
1683 let mut args = Vec::new();
1684 for i in 0..arg_count {
1685 args.push(self.stack[base + args_start + i].clone());
1686 }
1687
1688 let result = self.dispatch_method(obj, &method_name, &args)?;
1689 self.stack[base + a as usize] = result;
1690 }
1691
1692 Op::Throw => {
1693 let val = self.stack[base + a as usize].clone();
1695 self.thrown_value = Some(val.clone());
1696 let err_msg = format!("{val}");
1697 return Err(runtime_err(err_msg));
1698 }
1699
1700 Op::TryBegin => {
1701 let catch_ip = (self.frames[frame_idx].ip as i32 + sbx as i32) as usize;
1703 self.try_handlers.push(TryHandler {
1704 frame_idx: self.frames.len(),
1705 catch_ip,
1706 });
1707 }
1708
1709 Op::TryEnd => {
1710 self.try_handlers.pop();
1712 }
1713
1714 Op::Import => {
1715 #[cfg(feature = "native")]
1716 {
1717 let path = self.get_string_constant(frame_idx, bx)?;
1722 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1723 self.frames[frame_idx].ip += 1;
1724 let next_a = decode_a(next);
1725 let next_b = decode_b(next);
1726 let next_c = decode_c(next);
1727
1728 let result = if next_c == 0xAB {
1729 self.handle_use_import(&path, next_a, next_b, frame_idx)?
1731 } else {
1732 let alias_idx = next_a as u16;
1734 let alias = self.get_string_constant(frame_idx, alias_idx)?;
1735 self.handle_import(&path, &alias)?
1736 };
1737 self.stack[base + a as usize] = result;
1738 }
1739 #[cfg(not(feature = "native"))]
1740 {
1741 let _ = (a, bx, frame_idx);
1742 return Err(runtime_err("Module imports not available in WASM"));
1743 }
1744 }
1745
1746 Op::Await => {
1747 let val = self.stack[base + b as usize].clone();
1749 match val {
1750 VmValue::Task(task) => {
1751 let rx = {
1752 let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
1753 guard.take()
1754 };
1755 match rx {
1756 Some(receiver) => match receiver.recv() {
1757 Ok(Ok(result)) => {
1758 self.stack[base + a as usize] = result;
1759 }
1760 Ok(Err(err_msg)) => {
1761 return Err(runtime_err(err_msg));
1762 }
1763 Err(_) => {
1764 return Err(runtime_err("Task channel disconnected"));
1765 }
1766 },
1767 None => {
1768 return Err(runtime_err("Task already awaited"));
1769 }
1770 }
1771 }
1772 other => {
1774 self.stack[base + a as usize] = other;
1775 }
1776 }
1777 }
1778 Op::Yield => {
1779 let val = self.stack[base + a as usize].clone();
1781 self.yielded_value = Some(val.clone());
1782 self.yielded_ip = self.frames[frame_idx].ip;
1784 self.frames.pop();
1786 return Ok(Some(val));
1787 }
1788 Op::TryPropagate => {
1789 let src = self.stack[base + b as usize].clone();
1795 match &src {
1796 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
1797 if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
1798 self.stack[base + a as usize] = ei.fields[0].clone();
1800 } else if ei.variant.as_ref() == "Err" {
1801 self.frames.pop();
1803 return Ok(Some(src));
1804 } else {
1805 self.stack[base + a as usize] = src;
1806 }
1807 }
1808 VmValue::None => {
1809 self.frames.pop();
1811 return Ok(Some(VmValue::None));
1812 }
1813 _ => {
1814 self.stack[base + a as usize] = src;
1816 }
1817 }
1818 }
1819 Op::ExtractField => {
1820 let source = self.stack[base + b as usize].clone();
1823 let is_rest = (c & 0x80) != 0;
1824 let idx = (c & 0x7F) as usize;
1825 let val = if is_rest {
1826 match &source {
1828 VmValue::List(l) => {
1829 if idx < l.len() {
1830 VmValue::List(Box::new(l[idx..].to_vec()))
1831 } else {
1832 VmValue::List(Box::default())
1833 }
1834 }
1835 _ => VmValue::List(Box::default()),
1836 }
1837 } else {
1838 match &source {
1839 VmValue::EnumInstance(ei) => {
1840 ei.fields.get(idx).cloned().unwrap_or(VmValue::None)
1841 }
1842 VmValue::List(l) => l.get(idx).cloned().unwrap_or(VmValue::None),
1843 _ => VmValue::None,
1844 }
1845 };
1846 self.stack[base + a as usize] = val;
1847 }
1848 Op::ExtractNamedField => {
1849 let source = self.stack[base + b as usize].clone();
1851 let field_name = match &self.frames[frame_idx].prototype.constants[c as usize] {
1852 Constant::String(s) => s.clone(),
1853 _ => return Err(runtime_err("ExtractNamedField: expected string constant")),
1854 };
1855 let val = match &source {
1856 VmValue::StructInstance(s) => s
1857 .fields
1858 .iter()
1859 .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1860 .map(|(_, v)| v.clone())
1861 .unwrap_or(VmValue::None),
1862 VmValue::Map(m) => m
1863 .iter()
1864 .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1865 .map(|(_, v)| v.clone())
1866 .unwrap_or(VmValue::None),
1867 _ => VmValue::None,
1868 };
1869 self.stack[base + a as usize] = val;
1870 }
1871
1872 Op::LoadMoved => {
1874 self.stack[base + a as usize] = VmValue::Moved;
1875 }
1876 Op::MakeRef => {
1877 let val = self.stack[base + b as usize].clone();
1878 self.stack[base + a as usize] = VmValue::Ref(Arc::new(val));
1879 }
1880 Op::ParallelFor => {
1881 }
1884 Op::AgentExec => {
1885 #[cfg(feature = "native")]
1886 {
1887 let result = self.handle_agent_exec(frame_idx, b, c)?;
1888 self.stack[base + a as usize] = result;
1889 }
1890 #[cfg(not(feature = "native"))]
1891 {
1892 let _ = (a, b, c, frame_idx);
1893 return Err(runtime_err("Agents not available in WASM".to_string()));
1894 }
1895 }
1896 }
1897 Ok(None)
1898 }
1899
1900 fn do_call(
1902 &mut self,
1903 func: VmValue,
1904 caller_base: usize,
1905 func_reg: u8,
1906 args_start: u8,
1907 arg_count: u8,
1908 ) -> Result<(), TlError> {
1909 const MAX_CALL_DEPTH: usize = 512;
1910 if self.frames.len() >= MAX_CALL_DEPTH {
1911 return Err(runtime_err(
1912 "Stack overflow: maximum recursion depth (512) exceeded",
1913 ));
1914 }
1915 match func {
1916 VmValue::Function(closure) => {
1917 let proto = closure.prototype.clone();
1918 let arity = proto.arity as usize;
1919
1920 if arg_count as usize != arity {
1921 return Err(runtime_err(format!(
1922 "Expected {} arguments, got {}",
1923 arity, arg_count
1924 )));
1925 }
1926
1927 if proto.is_generator {
1929 let mut closed_upvalues = Vec::new();
1931 for uv in &closure.upvalues {
1932 match uv {
1933 UpvalueRef::Open { stack_index } => {
1934 let val = self.stack[*stack_index].clone();
1935 closed_upvalues.push(UpvalueRef::Closed(val));
1936 }
1937 UpvalueRef::Closed(v) => {
1938 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
1939 }
1940 }
1941 }
1942
1943 let num_regs = proto.num_registers as usize;
1945 let mut saved_stack = vec![VmValue::None; num_regs];
1946 for (i, slot) in saved_stack.iter_mut().enumerate().take(arg_count as usize) {
1947 *slot = self.stack[caller_base + args_start as usize + i].clone();
1948 }
1949
1950 let gn = VmGenerator::new(GeneratorKind::UserDefined {
1951 prototype: proto,
1952 upvalues: closed_upvalues,
1953 saved_stack,
1954 ip: 0,
1955 });
1956 self.stack[caller_base + func_reg as usize] =
1957 VmValue::Generator(Arc::new(Mutex::new(gn)));
1958 return Ok(());
1959 }
1960
1961 let new_base = self.stack.len();
1963 self.ensure_stack(new_base + proto.num_registers as usize + 1);
1964
1965 for i in 0..arg_count as usize {
1967 self.stack[new_base + i] =
1968 self.stack[caller_base + args_start as usize + i].clone();
1969 }
1970
1971 self.frames.push(CallFrame {
1972 prototype: proto,
1973 ip: 0,
1974 base: new_base,
1975 upvalues: closure.upvalues.clone(),
1976 });
1977
1978 let result = self.run()?;
1980
1981 let result = self.close_upvalues_in_value(result, new_base);
1983
1984 self.stack[caller_base + func_reg as usize] = result;
1986
1987 self.stack.truncate(new_base);
1989
1990 Ok(())
1991 }
1992 VmValue::Builtin(builtin_id) => {
1993 let result = self.call_builtin(
1994 builtin_id as u16,
1995 caller_base + args_start as usize,
1996 arg_count as usize,
1997 )?;
1998 self.stack[caller_base + func_reg as usize] = result;
1999 Ok(())
2000 }
2001 _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
2002 }
2003 }
2004
2005 fn value_may_need_closing(val: &VmValue) -> bool {
2010 match val {
2011 VmValue::Function(_) => true,
2012 VmValue::List(items) => items.iter().any(Self::value_may_need_closing),
2013 VmValue::Map(entries) => entries.iter().any(|(_, v)| Self::value_may_need_closing(v)),
2014 _ => false,
2015 }
2016 }
2017
2018 fn close_upvalues_in_value(&self, val: VmValue, frame_base: usize) -> VmValue {
2019 match val {
2020 VmValue::Function(ref closure) => {
2021 let needs_closing = closure.upvalues.iter().any(|uv| {
2023 matches!(uv, UpvalueRef::Open { stack_index } if *stack_index >= frame_base)
2024 });
2025 if !needs_closing {
2026 return val;
2027 }
2028 let closed_upvalues: Vec<UpvalueRef> = closure
2029 .upvalues
2030 .iter()
2031 .map(|uv| match uv {
2032 UpvalueRef::Open { stack_index } if *stack_index >= frame_base => {
2033 UpvalueRef::Closed(self.stack[*stack_index].clone())
2034 }
2035 other => other.clone(),
2036 })
2037 .collect();
2038 VmValue::Function(Arc::new(VmClosure {
2039 prototype: closure.prototype.clone(),
2040 upvalues: closed_upvalues,
2041 }))
2042 }
2043 VmValue::List(items) => {
2044 if !items.iter().any(Self::value_may_need_closing) {
2045 return VmValue::List(items);
2046 }
2047 VmValue::List(Box::new(
2048 (*items)
2049 .into_iter()
2050 .map(|v| self.close_upvalues_in_value(v, frame_base))
2051 .collect(),
2052 ))
2053 }
2054 VmValue::Map(entries) => {
2055 if !entries.iter().any(|(_, v)| Self::value_may_need_closing(v)) {
2056 return VmValue::Map(entries);
2057 }
2058 VmValue::Map(Box::new(
2059 (*entries)
2060 .into_iter()
2061 .map(|(k, v)| (k, self.close_upvalues_in_value(v, frame_base)))
2062 .collect(),
2063 ))
2064 }
2065 other => other,
2066 }
2067 }
2068
2069 pub(crate) fn execute_closure(
2071 &mut self,
2072 proto: &Arc<Prototype>,
2073 upvalues: &[UpvalueRef],
2074 ) -> Result<VmValue, TlError> {
2075 let base = self.stack.len();
2076 self.ensure_stack(base + proto.num_registers as usize + 1);
2077 self.frames.push(CallFrame {
2078 prototype: proto.clone(),
2079 ip: 0,
2080 base,
2081 upvalues: upvalues.to_vec(),
2082 });
2083 self.run()
2084 }
2085
2086 pub(crate) fn execute_closure_with_args(
2088 &mut self,
2089 proto: &Arc<Prototype>,
2090 upvalues: &[UpvalueRef],
2091 args: &[VmValue],
2092 ) -> Result<VmValue, TlError> {
2093 let base = self.stack.len();
2094 self.ensure_stack(base + proto.num_registers as usize + 1);
2095 for (i, arg) in args.iter().enumerate() {
2096 self.stack[base + i] = arg.clone();
2097 }
2098 self.frames.push(CallFrame {
2099 prototype: proto.clone(),
2100 ip: 0,
2101 base,
2102 upvalues: upvalues.to_vec(),
2103 });
2104 self.run()
2105 }
2106
2107 fn load_constant(&self, frame_idx: usize, idx: u16) -> Result<VmValue, TlError> {
2108 let frame = &self.frames[frame_idx];
2109 match &frame.prototype.constants[idx as usize] {
2110 Constant::Int(n) => Ok(VmValue::Int(*n)),
2111 Constant::Float(f) => Ok(VmValue::Float(*f)),
2112 Constant::String(s) => Ok(VmValue::String(s.clone())),
2113 Constant::Prototype(p) => {
2114 Ok(VmValue::Function(Arc::new(VmClosure {
2116 prototype: p.clone(),
2117 upvalues: Vec::new(),
2118 })))
2119 }
2120 Constant::Decimal(s) => {
2121 use std::str::FromStr;
2122 Ok(VmValue::Decimal(
2123 rust_decimal::Decimal::from_str(s).unwrap_or_default(),
2124 ))
2125 }
2126 Constant::AstExpr(_) | Constant::AstExprList(_) => Ok(VmValue::None),
2127 }
2128 }
2129
2130 fn get_string_constant(&self, frame_idx: usize, idx: u16) -> Result<Arc<str>, TlError> {
2131 let frame = &self.frames[frame_idx];
2132 match &frame.prototype.constants[idx as usize] {
2133 Constant::String(s) => Ok(s.clone()),
2134 _ => Err(runtime_err("Expected string constant")),
2135 }
2136 }
2137
2138 fn vm_add(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2141 let left = &self.stack[base + b as usize];
2142 let right = &self.stack[base + c as usize];
2143 match (left, right) {
2144 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2145 .checked_add(*b)
2146 .map(VmValue::Int)
2147 .unwrap_or_else(|| VmValue::Float(*a as f64 + *b as f64))),
2148 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a + b)),
2149 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 + b)),
2150 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a + *b as f64)),
2151 (VmValue::String(a), VmValue::String(b)) => {
2152 Ok(VmValue::String(Arc::from(format!("{a}{b}").as_str())))
2153 }
2154 #[cfg(feature = "gpu")]
2155 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2156 let a = a.clone();
2157 let b = b.clone();
2158 let ops = self.get_gpu_ops()?;
2159 let result = ops.add(&a, &b).map_err(runtime_err)?;
2160 Ok(VmValue::GpuTensor(Arc::new(result)))
2161 }
2162 #[cfg(feature = "gpu")]
2163 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2164 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2165 let lv = self.stack[base + b as usize].clone();
2166 let rv = self.stack[base + c as usize].clone();
2167 let a = self.ensure_gpu_tensor(&lv)?;
2168 let b_val = self.ensure_gpu_tensor(&rv)?;
2169 let ops = self.get_gpu_ops()?;
2170 let result = ops.add(&a, &b_val).map_err(runtime_err)?;
2171 Ok(VmValue::GpuTensor(Arc::new(result)))
2172 }
2173 #[cfg(feature = "native")]
2174 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2175 let result = a.add(b).map_err(|e| runtime_err(e.to_string()))?;
2176 Ok(VmValue::Tensor(Arc::new(result)))
2177 }
2178 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a + b)),
2180 (VmValue::Decimal(a), VmValue::Int(b)) => {
2181 Ok(VmValue::Decimal(a + rust_decimal::Decimal::from(*b)))
2182 }
2183 (VmValue::Int(a), VmValue::Decimal(b)) => {
2184 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) + b))
2185 }
2186 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) + b)),
2187 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a + decimal_to_f64(b))),
2188 _ => Err(runtime_err(format!(
2189 "Cannot apply `+` to {} and {}",
2190 left.type_name(),
2191 right.type_name()
2192 ))),
2193 }
2194 }
2195
2196 fn vm_sub(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2197 let left = &self.stack[base + b as usize];
2198 let right = &self.stack[base + c as usize];
2199 match (left, right) {
2200 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2201 .checked_sub(*b)
2202 .map(VmValue::Int)
2203 .unwrap_or_else(|| VmValue::Float(*a as f64 - *b as f64))),
2204 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a - b)),
2205 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 - b)),
2206 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a - *b as f64)),
2207 #[cfg(feature = "gpu")]
2208 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2209 let a = a.clone();
2210 let b = b.clone();
2211 let ops = self.get_gpu_ops()?;
2212 let result = ops.sub(&a, &b).map_err(runtime_err)?;
2213 Ok(VmValue::GpuTensor(Arc::new(result)))
2214 }
2215 #[cfg(feature = "gpu")]
2216 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2217 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2218 let lv = self.stack[base + b as usize].clone();
2219 let rv = self.stack[base + c as usize].clone();
2220 let a = self.ensure_gpu_tensor(&lv)?;
2221 let b_val = self.ensure_gpu_tensor(&rv)?;
2222 let ops = self.get_gpu_ops()?;
2223 let result = ops.sub(&a, &b_val).map_err(runtime_err)?;
2224 Ok(VmValue::GpuTensor(Arc::new(result)))
2225 }
2226 #[cfg(feature = "native")]
2227 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2228 let result = a.sub(b).map_err(|e| runtime_err(e.to_string()))?;
2229 Ok(VmValue::Tensor(Arc::new(result)))
2230 }
2231 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a - b)),
2232 (VmValue::Decimal(a), VmValue::Int(b)) => {
2233 Ok(VmValue::Decimal(a - rust_decimal::Decimal::from(*b)))
2234 }
2235 (VmValue::Int(a), VmValue::Decimal(b)) => {
2236 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) - b))
2237 }
2238 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) - b)),
2239 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a - decimal_to_f64(b))),
2240 _ => Err(runtime_err(format!(
2241 "Cannot apply `-` to {} and {}",
2242 left.type_name(),
2243 right.type_name()
2244 ))),
2245 }
2246 }
2247
2248 fn vm_mul(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2249 let left = &self.stack[base + b as usize];
2250 let right = &self.stack[base + c as usize];
2251 match (left, right) {
2252 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2253 .checked_mul(*b)
2254 .map(VmValue::Int)
2255 .unwrap_or_else(|| VmValue::Float(*a as f64 * *b as f64))),
2256 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a * b)),
2257 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 * b)),
2258 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a * *b as f64)),
2259 (VmValue::String(a), VmValue::Int(b)) => {
2260 if *b < 0 {
2261 return Err(runtime_err(
2262 "Cannot repeat string a negative number of times",
2263 ));
2264 }
2265 if *b > 10_000_000 {
2266 return Err(runtime_err(
2267 "String repeat count too large (max 10,000,000)",
2268 ));
2269 }
2270 Ok(VmValue::String(Arc::from(a.repeat(*b as usize).as_str())))
2271 }
2272 #[cfg(feature = "gpu")]
2273 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2274 let a = a.clone();
2275 let b = b.clone();
2276 let ops = self.get_gpu_ops()?;
2277 let result = ops.mul(&a, &b).map_err(runtime_err)?;
2278 Ok(VmValue::GpuTensor(Arc::new(result)))
2279 }
2280 #[cfg(feature = "gpu")]
2281 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2282 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2283 let lv = self.stack[base + b as usize].clone();
2284 let rv = self.stack[base + c as usize].clone();
2285 let a = self.ensure_gpu_tensor(&lv)?;
2286 let b_val = self.ensure_gpu_tensor(&rv)?;
2287 let ops = self.get_gpu_ops()?;
2288 let result = ops.mul(&a, &b_val).map_err(runtime_err)?;
2289 Ok(VmValue::GpuTensor(Arc::new(result)))
2290 }
2291 #[cfg(feature = "gpu")]
2292 (VmValue::GpuTensor(t), VmValue::Float(s))
2293 | (VmValue::Float(s), VmValue::GpuTensor(t)) => {
2294 let t = t.clone();
2295 let s = *s;
2296 let ops = self.get_gpu_ops()?;
2297 let result = ops.scale(&t, s as f32);
2298 Ok(VmValue::GpuTensor(Arc::new(result)))
2299 }
2300 #[cfg(feature = "native")]
2301 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2302 let result = a.mul(b).map_err(|e| runtime_err(e.to_string()))?;
2303 Ok(VmValue::Tensor(Arc::new(result)))
2304 }
2305 #[cfg(feature = "native")]
2306 (VmValue::Tensor(t), VmValue::Float(s)) | (VmValue::Float(s), VmValue::Tensor(t)) => {
2307 let result = t.scale(*s);
2308 Ok(VmValue::Tensor(Arc::new(result)))
2309 }
2310 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a * b)),
2311 (VmValue::Decimal(a), VmValue::Int(b)) => {
2312 Ok(VmValue::Decimal(a * rust_decimal::Decimal::from(*b)))
2313 }
2314 (VmValue::Int(a), VmValue::Decimal(b)) => {
2315 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) * b))
2316 }
2317 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) * b)),
2318 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a * decimal_to_f64(b))),
2319 _ => Err(runtime_err(format!(
2320 "Cannot apply `*` to {} and {}",
2321 left.type_name(),
2322 right.type_name()
2323 ))),
2324 }
2325 }
2326
2327 fn vm_div(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2328 let left = &self.stack[base + b as usize];
2329 let right = &self.stack[base + c as usize];
2330 match (left, right) {
2331 (VmValue::Int(a), VmValue::Int(b)) => {
2332 if *b == 0 {
2333 return Err(runtime_err("Division by zero"));
2334 }
2335 Ok(VmValue::Int(a / b))
2336 }
2337 (VmValue::Float(a), VmValue::Float(b)) => {
2338 if *b == 0.0 {
2339 return Err(runtime_err("Division by zero"));
2340 }
2341 Ok(VmValue::Float(a / b))
2342 }
2343 (VmValue::Int(a), VmValue::Float(b)) => {
2344 if *b == 0.0 {
2345 return Err(runtime_err("Division by zero"));
2346 }
2347 Ok(VmValue::Float(*a as f64 / b))
2348 }
2349 (VmValue::Float(a), VmValue::Int(b)) => {
2350 if *b == 0 {
2351 return Err(runtime_err("Division by zero"));
2352 }
2353 Ok(VmValue::Float(a / *b as f64))
2354 }
2355 #[cfg(feature = "gpu")]
2356 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2357 let a = a.clone();
2358 let b = b.clone();
2359 let ops = self.get_gpu_ops()?;
2360 let result = ops.div(&a, &b).map_err(runtime_err)?;
2361 Ok(VmValue::GpuTensor(Arc::new(result)))
2362 }
2363 #[cfg(feature = "gpu")]
2364 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2365 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2366 let lv = self.stack[base + b as usize].clone();
2367 let rv = self.stack[base + c as usize].clone();
2368 let a = self.ensure_gpu_tensor(&lv)?;
2369 let b_val = self.ensure_gpu_tensor(&rv)?;
2370 let ops = self.get_gpu_ops()?;
2371 let result = ops.div(&a, &b_val).map_err(runtime_err)?;
2372 Ok(VmValue::GpuTensor(Arc::new(result)))
2373 }
2374 #[cfg(feature = "native")]
2375 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2376 let result = a.div(b).map_err(|e| runtime_err(e.to_string()))?;
2377 Ok(VmValue::Tensor(Arc::new(result)))
2378 }
2379 (VmValue::Decimal(a), VmValue::Decimal(b)) => {
2380 if b.is_zero() {
2381 return Err(runtime_err("Division by zero"));
2382 }
2383 Ok(VmValue::Decimal(a / b))
2384 }
2385 (VmValue::Decimal(a), VmValue::Int(b)) => {
2386 if *b == 0 {
2387 return Err(runtime_err("Division by zero"));
2388 }
2389 Ok(VmValue::Decimal(a / rust_decimal::Decimal::from(*b)))
2390 }
2391 (VmValue::Int(a), VmValue::Decimal(b)) => {
2392 if b.is_zero() {
2393 return Err(runtime_err("Division by zero"));
2394 }
2395 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) / b))
2396 }
2397 (VmValue::Decimal(a), VmValue::Float(b)) => {
2398 if *b == 0.0 {
2399 return Err(runtime_err("Division by zero"));
2400 }
2401 Ok(VmValue::Float(decimal_to_f64(a) / b))
2402 }
2403 (VmValue::Float(a), VmValue::Decimal(b)) => {
2404 if b.is_zero() {
2405 return Err(runtime_err("Division by zero"));
2406 }
2407 Ok(VmValue::Float(a / decimal_to_f64(b)))
2408 }
2409 _ => Err(runtime_err(format!(
2410 "Cannot apply `/` to {} and {}",
2411 left.type_name(),
2412 right.type_name()
2413 ))),
2414 }
2415 }
2416
2417 fn vm_mod(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2418 let left = &self.stack[base + b as usize];
2419 let right = &self.stack[base + c as usize];
2420 match (left, right) {
2421 (VmValue::Int(a), VmValue::Int(b)) => {
2422 if *b == 0 {
2423 return Err(runtime_err("Modulo by zero"));
2424 }
2425 Ok(VmValue::Int(a % b))
2426 }
2427 (VmValue::Float(a), VmValue::Float(b)) => {
2428 if *b == 0.0 {
2429 return Err(runtime_err("Modulo by zero"));
2430 }
2431 Ok(VmValue::Float(a % b))
2432 }
2433 (VmValue::Int(a), VmValue::Float(b)) => {
2434 if *b == 0.0 {
2435 return Err(runtime_err("Modulo by zero"));
2436 }
2437 Ok(VmValue::Float(*a as f64 % b))
2438 }
2439 (VmValue::Float(a), VmValue::Int(b)) => {
2440 if *b == 0 {
2441 return Err(runtime_err("Modulo by zero"));
2442 }
2443 Ok(VmValue::Float(a % *b as f64))
2444 }
2445 _ => Err(runtime_err(format!(
2446 "Cannot apply `%` to {} and {}",
2447 left.type_name(),
2448 right.type_name()
2449 ))),
2450 }
2451 }
2452
2453 fn vm_pow(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2454 let left = &self.stack[base + b as usize];
2455 let right = &self.stack[base + c as usize];
2456 match (left, right) {
2457 (VmValue::Int(a), VmValue::Int(b)) => {
2458 if *b < 0 {
2459 return Ok(VmValue::Float((*a as f64).powi(*b as i32)));
2460 }
2461 match a.checked_pow(*b as u32) {
2462 Some(result) => Ok(VmValue::Int(result)),
2463 None => Ok(VmValue::Float((*a as f64).powf(*b as f64))),
2464 }
2465 }
2466 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
2467 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float((*a as f64).powf(*b))),
2468 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a.powf(*b as f64))),
2469 _ => Err(runtime_err(format!(
2470 "Cannot apply `**` to {} and {}",
2471 left.type_name(),
2472 right.type_name()
2473 ))),
2474 }
2475 }
2476
2477 fn vm_eq(&self, base: usize, b: u8, c: u8) -> bool {
2478 self.stack[base + b as usize] == self.stack[base + c as usize]
2479 }
2480
2481 fn vm_cmp(&self, base: usize, b: u8, c: u8) -> Result<Option<i8>, TlError> {
2482 let left = &self.stack[base + b as usize];
2483 let right = &self.stack[base + c as usize];
2484 match (left, right) {
2485 (VmValue::Int(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2486 (VmValue::Float(a), VmValue::Float(b)) => Ok(a.partial_cmp(b).map(|o| o as i8)),
2487 (VmValue::Int(a), VmValue::Float(b)) => {
2488 let fa = *a as f64;
2489 Ok(fa.partial_cmp(b).map(|o| o as i8))
2490 }
2491 (VmValue::Float(a), VmValue::Int(b)) => {
2492 let fb = *b as f64;
2493 Ok(a.partial_cmp(&fb).map(|o| o as i8))
2494 }
2495 (VmValue::String(a), VmValue::String(b)) => Ok(Some(a.cmp(b) as i8)),
2496 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(Some(a.cmp(b) as i8)),
2497 (VmValue::Decimal(a), VmValue::Int(b)) => {
2498 Ok(Some(a.cmp(&rust_decimal::Decimal::from(*b)) as i8))
2499 }
2500 (VmValue::Int(a), VmValue::Decimal(b)) => {
2501 Ok(Some(rust_decimal::Decimal::from(*a).cmp(b) as i8))
2502 }
2503 (VmValue::DateTime(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2504 (VmValue::DateTime(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2505 (VmValue::Int(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2506 _ => Err(runtime_err(format!(
2507 "Cannot compare {} and {}",
2508 left.type_name(),
2509 right.type_name()
2510 ))),
2511 }
2512 }
2513
2514 fn check_permission(&self, perm: &str) -> Result<(), TlError> {
2517 if let Some(ref policy) = self.security_policy
2518 && !policy.check(perm)
2519 {
2520 return Err(runtime_err(format!("{perm} blocked by security policy")));
2521 }
2522 Ok(())
2523 }
2524
2525 pub fn call_builtin(
2528 &mut self,
2529 id: u16,
2530 args_base: usize,
2531 arg_count: usize,
2532 ) -> Result<VmValue, TlError> {
2533 let args: Vec<VmValue> = (0..arg_count)
2534 .map(|i| {
2535 let val = &self.stack[args_base + i];
2536 match val {
2538 VmValue::Ref(inner) => inner.as_ref().clone(),
2539 other => other.clone(),
2540 }
2541 })
2542 .collect();
2543
2544 let builtin_id: BuiltinId =
2545 BuiltinId::try_from(id).map_err(|v| runtime_err(format!("Invalid builtin id: {v}")))?;
2546
2547 match builtin_id {
2548 BuiltinId::Print | BuiltinId::Println => {
2549 let mut parts = Vec::new();
2550 for a in &args {
2551 #[cfg(feature = "native")]
2552 match a {
2553 VmValue::Table(t) => {
2554 let batches =
2555 self.engine().collect(t.df.clone()).map_err(runtime_err)?;
2556 let formatted =
2557 DataEngine::format_batches(&batches).map_err(runtime_err)?;
2558 parts.push(formatted);
2559 }
2560 _ => parts.push(format!("{a}")),
2561 }
2562 #[cfg(not(feature = "native"))]
2563 parts.push(format!("{a}"));
2564 }
2565 let line = parts.join(" ");
2566 println!("{line}");
2567 self.output.push(line);
2568 Ok(VmValue::None)
2569 }
2570 BuiltinId::Len => match args.first() {
2571 Some(VmValue::String(s)) => Ok(VmValue::Int(s.len() as i64)),
2572 Some(VmValue::List(l)) => Ok(VmValue::Int(l.len() as i64)),
2573 Some(VmValue::Map(pairs)) => Ok(VmValue::Int(pairs.len() as i64)),
2574 Some(VmValue::Set(items)) => Ok(VmValue::Int(items.len() as i64)),
2575 _ => Err(runtime_err("len() expects a string, list, map, or set")),
2576 },
2577 BuiltinId::Str => Ok(VmValue::String(Arc::from(
2578 args.first()
2579 .map(|v| format!("{v}"))
2580 .unwrap_or_default()
2581 .as_str(),
2582 ))),
2583 BuiltinId::Int => match args.first() {
2584 Some(VmValue::Float(f)) => Ok(VmValue::Int(*f as i64)),
2585 Some(VmValue::String(s)) => s
2586 .parse::<i64>()
2587 .map(VmValue::Int)
2588 .map_err(|_| runtime_err(format!("Cannot convert '{s}' to int"))),
2589 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
2590 Some(VmValue::Bool(b)) => Ok(VmValue::Int(if *b { 1 } else { 0 })),
2591 _ => Err(runtime_err("int() expects a number, string, or bool")),
2592 },
2593 BuiltinId::Float => match args.first() {
2594 Some(VmValue::Int(n)) => Ok(VmValue::Float(*n as f64)),
2595 Some(VmValue::String(s)) => s
2596 .parse::<f64>()
2597 .map(VmValue::Float)
2598 .map_err(|_| runtime_err(format!("Cannot convert '{s}' to float"))),
2599 Some(VmValue::Float(n)) => Ok(VmValue::Float(*n)),
2600 Some(VmValue::Bool(b)) => Ok(VmValue::Float(if *b { 1.0 } else { 0.0 })),
2601 _ => Err(runtime_err("float() expects a number, string, or bool")),
2602 },
2603 BuiltinId::Abs => match args.first() {
2604 Some(VmValue::Int(n)) => Ok(VmValue::Int(n.abs())),
2605 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.abs())),
2606 _ => Err(runtime_err("abs() expects a number")),
2607 },
2608 BuiltinId::Min => {
2609 if args.len() == 2 {
2610 match (&args[0], &args[1]) {
2611 (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.min(b))),
2612 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.min(*b))),
2613 _ => Err(runtime_err("min() expects two numbers")),
2614 }
2615 } else {
2616 Err(runtime_err("min() expects 2 arguments"))
2617 }
2618 }
2619 BuiltinId::Max => {
2620 if args.len() == 2 {
2621 match (&args[0], &args[1]) {
2622 (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.max(b))),
2623 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.max(*b))),
2624 _ => Err(runtime_err("max() expects two numbers")),
2625 }
2626 } else {
2627 Err(runtime_err("max() expects 2 arguments"))
2628 }
2629 }
2630 BuiltinId::Range => {
2631 if args.len() == 1 {
2632 if let VmValue::Int(n) = &args[0] {
2633 if *n > 10_000_000 {
2634 return Err(runtime_err("range() size too large (max 10,000,000)"));
2635 }
2636 if *n < 0 {
2637 return Ok(VmValue::List(Box::default()));
2638 }
2639 Ok(VmValue::List(Box::new((0..*n).map(VmValue::Int).collect())))
2640 } else {
2641 Err(runtime_err("range() expects an integer"))
2642 }
2643 } else if args.len() == 2 {
2644 if let (VmValue::Int(start), VmValue::Int(end)) = (&args[0], &args[1]) {
2645 let size = (*end - *start).max(0);
2646 if size > 10_000_000 {
2647 return Err(runtime_err("range() size too large (max 10,000,000)"));
2648 }
2649 Ok(VmValue::List(Box::new(
2650 (*start..*end).map(VmValue::Int).collect(),
2651 )))
2652 } else {
2653 Err(runtime_err("range() expects integers"))
2654 }
2655 } else if args.len() == 3 {
2656 if let (VmValue::Int(start), VmValue::Int(end), VmValue::Int(step)) =
2657 (&args[0], &args[1], &args[2])
2658 {
2659 if *step == 0 {
2660 return Err(runtime_err("range() step cannot be zero"));
2661 }
2662 let mut result = Vec::new();
2663 let mut i = *start;
2664 if *step > 0 {
2665 while i < *end {
2666 result.push(VmValue::Int(i));
2667 i += step;
2668 }
2669 } else {
2670 while i > *end {
2671 result.push(VmValue::Int(i));
2672 i += step;
2673 }
2674 }
2675 Ok(VmValue::List(Box::new(result)))
2676 } else {
2677 Err(runtime_err("range() expects integers"))
2678 }
2679 } else {
2680 Err(runtime_err("range() expects 1, 2, or 3 arguments"))
2681 }
2682 }
2683 BuiltinId::Push => {
2684 if args.len() == 2 {
2685 if let VmValue::List(mut items) = args[0].clone() {
2686 items.push(args[1].clone());
2687 Ok(VmValue::List(items))
2688 } else {
2689 Err(runtime_err("push() first arg must be a list"))
2690 }
2691 } else {
2692 Err(runtime_err("push() expects 2 arguments"))
2693 }
2694 }
2695 BuiltinId::TypeOf => Ok(VmValue::String(Arc::from(
2696 args.first().map(|v| v.type_name()).unwrap_or("none"),
2697 ))),
2698 BuiltinId::Map => {
2699 if args.len() != 2 {
2700 return Err(runtime_err("map() expects 2 arguments (list, fn)"));
2701 }
2702 let items = match &args[0] {
2703 VmValue::List(items) => (**items).clone(),
2704 _ => return Err(runtime_err("map() first arg must be a list")),
2705 };
2706 let func = args[1].clone();
2707 #[cfg(feature = "native")]
2709 if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2710 let proto = match &func {
2711 VmValue::Function(c) => c.prototype.clone(),
2712 _ => unreachable!(),
2713 };
2714 let result: Result<Vec<VmValue>, TlError> = items
2715 .into_par_iter()
2716 .map(|item| execute_pure_fn(&proto, &[item]))
2717 .collect();
2718 return Ok(VmValue::List(Box::new(result?)));
2719 }
2720 let mut result = Vec::new();
2721 for item in items {
2722 let val = self.call_vm_function(&func, &[item])?;
2723 result.push(val);
2724 }
2725 Ok(VmValue::List(Box::new(result)))
2726 }
2727 BuiltinId::Filter => {
2728 if args.len() != 2 {
2729 return Err(runtime_err("filter() expects 2 arguments (list, fn)"));
2730 }
2731 let items = match &args[0] {
2732 VmValue::List(items) => (**items).clone(),
2733 _ => return Err(runtime_err("filter() first arg must be a list")),
2734 };
2735 let func = args[1].clone();
2736 #[cfg(feature = "native")]
2738 if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2739 let proto = match &func {
2740 VmValue::Function(c) => c.prototype.clone(),
2741 _ => unreachable!(),
2742 };
2743 let result: Result<Vec<VmValue>, TlError> = items
2744 .into_par_iter()
2745 .filter_map(|item| {
2746 match execute_pure_fn(&proto, std::slice::from_ref(&item)) {
2747 Ok(val) => {
2748 if val.is_truthy() {
2749 Some(Ok(item))
2750 } else {
2751 None
2752 }
2753 }
2754 Err(e) => Some(Err(e)),
2755 }
2756 })
2757 .collect();
2758 return Ok(VmValue::List(Box::new(result?)));
2759 }
2760 let mut result = Vec::new();
2761 for item in items {
2762 let val = self.call_vm_function(&func, std::slice::from_ref(&item))?;
2763 if val.is_truthy() {
2764 result.push(item);
2765 }
2766 }
2767 Ok(VmValue::List(Box::new(result)))
2768 }
2769 BuiltinId::Reduce | BuiltinId::Fold => {
2770 if args.len() != 3 {
2771 return Err(runtime_err(
2772 "reduce()/fold() expects 3 arguments (list, init, fn)",
2773 ));
2774 }
2775 let items = match &args[0] {
2776 VmValue::List(items) => (**items).clone(),
2777 _ => return Err(runtime_err("reduce() first arg must be a list")),
2778 };
2779 let mut acc = args[1].clone();
2780 let func = args[2].clone();
2781 for item in items {
2782 acc = self.call_vm_function(&func, &[acc, item])?;
2783 }
2784 Ok(acc)
2785 }
2786 BuiltinId::Sum => {
2787 if args.len() != 1 {
2788 return Err(runtime_err("sum() expects 1 argument (list)"));
2789 }
2790 let items = match &args[0] {
2791 VmValue::List(items) => items,
2792 _ => return Err(runtime_err("sum() expects a list")),
2793 };
2794 let has_float = items.iter().any(|v| matches!(v, VmValue::Float(_)));
2796 #[cfg(feature = "native")]
2797 if items.len() >= PARALLEL_THRESHOLD {
2798 if has_float {
2800 let total: f64 = items
2801 .par_iter()
2802 .map(|v| match v {
2803 VmValue::Int(n) => *n as f64,
2804 VmValue::Float(n) => *n,
2805 _ => 0.0,
2806 })
2807 .sum();
2808 return Ok(VmValue::Float(total));
2809 } else {
2810 let total: i64 = items
2811 .par_iter()
2812 .map(|v| match v {
2813 VmValue::Int(n) => *n,
2814 _ => 0,
2815 })
2816 .sum();
2817 return Ok(VmValue::Int(total));
2818 }
2819 }
2820 let mut total: i64 = 0;
2822 let mut is_float = false;
2823 let mut total_f: f64 = 0.0;
2824 for item in items.iter() {
2825 match item {
2826 VmValue::Int(n) => {
2827 if is_float {
2828 total_f += *n as f64;
2829 } else {
2830 total += n;
2831 }
2832 }
2833 VmValue::Float(n) => {
2834 if !is_float {
2835 total_f = total as f64;
2836 is_float = true;
2837 }
2838 total_f += n;
2839 }
2840 _ => return Err(runtime_err("sum() list must contain numbers")),
2841 }
2842 }
2843 if is_float {
2844 Ok(VmValue::Float(total_f))
2845 } else {
2846 Ok(VmValue::Int(total))
2847 }
2848 }
2849 BuiltinId::Any => {
2850 if args.len() != 2 {
2851 return Err(runtime_err("any() expects 2 arguments (list, fn)"));
2852 }
2853 let items = match &args[0] {
2854 VmValue::List(items) => (**items).clone(),
2855 _ => return Err(runtime_err("any() first arg must be a list")),
2856 };
2857 let func = args[1].clone();
2858 for item in items {
2859 let val = self.call_vm_function(&func, &[item])?;
2860 if val.is_truthy() {
2861 return Ok(VmValue::Bool(true));
2862 }
2863 }
2864 Ok(VmValue::Bool(false))
2865 }
2866 BuiltinId::All => {
2867 if args.len() != 2 {
2868 return Err(runtime_err("all() expects 2 arguments (list, fn)"));
2869 }
2870 let items = match &args[0] {
2871 VmValue::List(items) => (**items).clone(),
2872 _ => return Err(runtime_err("all() first arg must be a list")),
2873 };
2874 let func = args[1].clone();
2875 for item in items {
2876 let val = self.call_vm_function(&func, &[item])?;
2877 if !val.is_truthy() {
2878 return Ok(VmValue::Bool(false));
2879 }
2880 }
2881 Ok(VmValue::Bool(true))
2882 }
2883 #[cfg(feature = "native")]
2885 BuiltinId::ReadCsv => {
2886 if args.len() != 1 {
2887 return Err(runtime_err("read_csv() expects 1 argument (path)"));
2888 }
2889 let path = match &args[0] {
2890 VmValue::String(s) => s.to_string(),
2891 _ => return Err(runtime_err("read_csv() path must be a string")),
2892 };
2893 match self.engine().read_csv(&path) {
2894 Ok(df) => Ok(VmValue::Table(VmTable { df })),
2895 Err(e) => {
2896 let msg = e.to_string();
2897 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2898 type_name: Arc::from("DataError"),
2899 variant: Arc::from("ParseError"),
2900 fields: vec![
2901 VmValue::String(Arc::from(msg.as_str())),
2902 VmValue::String(Arc::from(path.as_str())),
2903 ],
2904 })));
2905 Err(runtime_err(msg))
2906 }
2907 }
2908 }
2909 #[cfg(feature = "native")]
2910 BuiltinId::ReadParquet => {
2911 if args.len() != 1 {
2912 return Err(runtime_err("read_parquet() expects 1 argument (path)"));
2913 }
2914 let path = match &args[0] {
2915 VmValue::String(s) => s.to_string(),
2916 _ => return Err(runtime_err("read_parquet() path must be a string")),
2917 };
2918 match self.engine().read_parquet(&path) {
2919 Ok(df) => Ok(VmValue::Table(VmTable { df })),
2920 Err(e) => {
2921 let msg = e.to_string();
2922 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2923 type_name: Arc::from("DataError"),
2924 variant: Arc::from("ParseError"),
2925 fields: vec![
2926 VmValue::String(Arc::from(msg.as_str())),
2927 VmValue::String(Arc::from(path.as_str())),
2928 ],
2929 })));
2930 Err(runtime_err(msg))
2931 }
2932 }
2933 }
2934 #[cfg(feature = "native")]
2935 BuiltinId::WriteCsv => {
2936 if args.len() != 2 {
2937 return Err(runtime_err("write_csv() expects 2 arguments (table, path)"));
2938 }
2939 let df = match &args[0] {
2940 VmValue::Table(t) => t.df.clone(),
2941 _ => return Err(runtime_err("write_csv() first arg must be a table")),
2942 };
2943 let path = match &args[1] {
2944 VmValue::String(s) => s.to_string(),
2945 _ => return Err(runtime_err("write_csv() path must be a string")),
2946 };
2947 match self.engine().write_csv(df, &path) {
2948 Ok(_) => Ok(VmValue::None),
2949 Err(e) => {
2950 let msg = e.to_string();
2951 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2952 type_name: Arc::from("DataError"),
2953 variant: Arc::from("ParseError"),
2954 fields: vec![
2955 VmValue::String(Arc::from(msg.as_str())),
2956 VmValue::String(Arc::from(path.as_str())),
2957 ],
2958 })));
2959 Err(runtime_err(msg))
2960 }
2961 }
2962 }
2963 #[cfg(feature = "native")]
2964 BuiltinId::WriteParquet => {
2965 if args.len() != 2 {
2966 return Err(runtime_err(
2967 "write_parquet() expects 2 arguments (table, path)",
2968 ));
2969 }
2970 let df = match &args[0] {
2971 VmValue::Table(t) => t.df.clone(),
2972 _ => return Err(runtime_err("write_parquet() first arg must be a table")),
2973 };
2974 let path = match &args[1] {
2975 VmValue::String(s) => s.to_string(),
2976 _ => return Err(runtime_err("write_parquet() path must be a string")),
2977 };
2978 match self.engine().write_parquet(df, &path) {
2979 Ok(_) => Ok(VmValue::None),
2980 Err(e) => {
2981 let msg = e.to_string();
2982 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2983 type_name: Arc::from("DataError"),
2984 variant: Arc::from("ParseError"),
2985 fields: vec![
2986 VmValue::String(Arc::from(msg.as_str())),
2987 VmValue::String(Arc::from(path.as_str())),
2988 ],
2989 })));
2990 Err(runtime_err(msg))
2991 }
2992 }
2993 }
2994 #[cfg(feature = "native")]
2995 BuiltinId::Collect => {
2996 if args.len() != 1 {
2997 return Err(runtime_err("collect() expects 1 argument (table)"));
2998 }
2999 let df = match &args[0] {
3000 VmValue::Table(t) => t.df.clone(),
3001 _ => return Err(runtime_err("collect() expects a table")),
3002 };
3003 let batches = self.engine().collect(df).map_err(runtime_err)?;
3004 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3005 Ok(VmValue::String(Arc::from(formatted.as_str())))
3006 }
3007 #[cfg(feature = "native")]
3008 BuiltinId::ToRows => {
3009 use tl_data::datafusion::arrow::array::{
3010 Array, BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array,
3011 LargeStringArray, StringArray, UInt32Array, UInt64Array,
3012 };
3013 if args.len() != 1 {
3014 return Err(runtime_err("to_rows() expects 1 argument (table)"));
3015 }
3016 let df = match &args[0] {
3017 VmValue::Table(t) => t.df.clone(),
3018 _ => return Err(runtime_err("to_rows() expects a table")),
3019 };
3020 let batches = self.engine().collect(df).map_err(runtime_err)?;
3021 let mut rows: Vec<VmValue> = Vec::new();
3022 for batch in &batches {
3023 let schema = batch.schema();
3024 let num_rows = batch.num_rows();
3025 for row_idx in 0..num_rows {
3026 let mut map: Vec<(Arc<str>, VmValue)> = Vec::new();
3027 for col_idx in 0..batch.num_columns() {
3028 let col_name: Arc<str> =
3029 Arc::from(schema.field(col_idx).name().as_str());
3030 let col = batch.column(col_idx);
3031 let val = if col.is_null(row_idx) {
3032 VmValue::None
3033 } else if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
3034 VmValue::Float(arr.value(row_idx))
3035 } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
3036 VmValue::Float(arr.value(row_idx) as f64)
3037 } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
3038 VmValue::Int(arr.value(row_idx))
3039 } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
3040 VmValue::Int(arr.value(row_idx) as i64)
3041 } else if let Some(arr) = col.as_any().downcast_ref::<UInt64Array>() {
3042 VmValue::Int(arr.value(row_idx) as i64)
3043 } else if let Some(arr) = col.as_any().downcast_ref::<UInt32Array>() {
3044 VmValue::Int(arr.value(row_idx) as i64)
3045 } else if let Some(arr) = col.as_any().downcast_ref::<StringArray>() {
3046 VmValue::String(Arc::from(arr.value(row_idx)))
3047 } else if let Some(arr) =
3048 col.as_any().downcast_ref::<LargeStringArray>()
3049 {
3050 VmValue::String(Arc::from(arr.value(row_idx)))
3051 } else if let Some(arr) = col.as_any().downcast_ref::<BooleanArray>() {
3052 VmValue::Bool(arr.value(row_idx))
3053 } else {
3054 VmValue::String(Arc::from(
3055 format!("{:?}", col.data_type()).as_str(),
3056 ))
3057 };
3058 map.push((col_name, val));
3059 }
3060 rows.push(VmValue::Map(Box::new(map)));
3061 }
3062 }
3063 Ok(VmValue::List(Box::new(rows)))
3064 }
3065 #[cfg(feature = "native")]
3066 BuiltinId::Show => {
3067 let df = match args.first() {
3068 Some(VmValue::Table(t)) => t.df.clone(),
3069 _ => return Err(runtime_err("show() expects a table")),
3070 };
3071 let limit = match args.get(1) {
3072 Some(VmValue::Int(n)) => *n as usize,
3073 None => 20,
3074 _ => return Err(runtime_err("show() second arg must be an int")),
3075 };
3076 let limited = df
3077 .limit(0, Some(limit))
3078 .map_err(|e| runtime_err(format!("{e}")))?;
3079 let batches = self.engine().collect(limited).map_err(runtime_err)?;
3080 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3081 println!("{formatted}");
3082 self.output.push(formatted);
3083 Ok(VmValue::None)
3084 }
3085 #[cfg(feature = "native")]
3086 BuiltinId::Describe => {
3087 if args.len() != 1 {
3088 return Err(runtime_err("describe() expects 1 argument (table)"));
3089 }
3090 let df = match &args[0] {
3091 VmValue::Table(t) => t.df.clone(),
3092 _ => return Err(runtime_err("describe() expects a table")),
3093 };
3094 let schema = df.schema();
3095 let mut lines = Vec::new();
3096 lines.push("Columns:".to_string());
3097 for (qualifier, field) in schema.iter() {
3098 let prefix = match qualifier {
3099 Some(q) => format!("{q}."),
3100 None => String::new(),
3101 };
3102 lines.push(format!(
3103 " {}{}: {}",
3104 prefix,
3105 field.name(),
3106 field.data_type()
3107 ));
3108 }
3109 let output = lines.join("\n");
3110 println!("{output}");
3111 self.output.push(output.clone());
3112 Ok(VmValue::String(Arc::from(output.as_str())))
3113 }
3114 #[cfg(feature = "native")]
3115 BuiltinId::Head => {
3116 if args.is_empty() {
3117 return Err(runtime_err("head() expects at least 1 argument (table)"));
3118 }
3119 let df = match &args[0] {
3120 VmValue::Table(t) => t.df.clone(),
3121 _ => return Err(runtime_err("head() first arg must be a table")),
3122 };
3123 let n = match args.get(1) {
3124 Some(VmValue::Int(n)) => *n as usize,
3125 None => 10,
3126 _ => return Err(runtime_err("head() second arg must be an int")),
3127 };
3128 let limited = df
3129 .limit(0, Some(n))
3130 .map_err(|e| runtime_err(format!("{e}")))?;
3131 Ok(VmValue::Table(VmTable { df: limited }))
3132 }
3133 #[cfg(feature = "native")]
3134 BuiltinId::Postgres => {
3135 if args.len() != 2 {
3136 return Err(runtime_err(
3137 "postgres() expects 2 arguments (conn_str, table_name)",
3138 ));
3139 }
3140 let conn_str = match &args[0] {
3141 VmValue::String(s) => s.to_string(),
3142 _ => return Err(runtime_err("postgres() conn_str must be a string")),
3143 };
3144 let table_name = match &args[1] {
3145 VmValue::String(s) => s.to_string(),
3146 _ => return Err(runtime_err("postgres() table_name must be a string")),
3147 };
3148 let conn_str = resolve_tl_config_connection(&conn_str);
3149 match self.engine().read_postgres(&conn_str, &table_name) {
3150 Ok(df) => Ok(VmValue::Table(VmTable { df })),
3151 Err(e) => {
3152 let msg = e.to_string();
3153 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3154 type_name: Arc::from("ConnectorError"),
3155 variant: Arc::from("QueryError"),
3156 fields: vec![
3157 VmValue::String(Arc::from(msg.as_str())),
3158 VmValue::String(Arc::from("postgres")),
3159 ],
3160 })));
3161 Err(runtime_err(msg))
3162 }
3163 }
3164 }
3165 #[cfg(feature = "native")]
3166 BuiltinId::PostgresQuery => {
3167 if args.len() != 2 {
3168 return Err(runtime_err(
3169 "postgres_query() expects 2 arguments (conn_str, query)",
3170 ));
3171 }
3172 let conn_str = match &args[0] {
3173 VmValue::String(s) => s.to_string(),
3174 _ => return Err(runtime_err("postgres_query() conn_str must be a string")),
3175 };
3176 let query = match &args[1] {
3177 VmValue::String(s) => s.to_string(),
3178 _ => return Err(runtime_err("postgres_query() query must be a string")),
3179 };
3180 let conn_str = resolve_tl_config_connection(&conn_str);
3181 match self
3182 .engine()
3183 .query_postgres(&conn_str, &query, "__pg_query_result")
3184 {
3185 Ok(df) => Ok(VmValue::Table(VmTable { df })),
3186 Err(e) => {
3187 let msg = e.to_string();
3188 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3189 type_name: Arc::from("ConnectorError"),
3190 variant: Arc::from("QueryError"),
3191 fields: vec![
3192 VmValue::String(Arc::from(msg.as_str())),
3193 VmValue::String(Arc::from("postgres")),
3194 ],
3195 })));
3196 Err(runtime_err(msg))
3197 }
3198 }
3199 }
3200 BuiltinId::TlConfigResolve => {
3201 if args.len() != 1 {
3202 return Err(runtime_err("tl_config_resolve() expects 1 argument (name)"));
3203 }
3204 let name = match &args[0] {
3205 VmValue::String(s) => s.to_string(),
3206 _ => return Err(runtime_err("tl_config_resolve() name must be a string")),
3207 };
3208 let resolved = resolve_tl_config_connection(&name);
3209 Ok(VmValue::String(Arc::from(resolved.as_str())))
3210 }
3211 #[cfg(not(feature = "native"))]
3212 BuiltinId::ReadCsv
3213 | BuiltinId::ReadParquet
3214 | BuiltinId::WriteCsv
3215 | BuiltinId::WriteParquet
3216 | BuiltinId::Collect
3217 | BuiltinId::ToRows
3218 | BuiltinId::Show
3219 | BuiltinId::Describe
3220 | BuiltinId::Head
3221 | BuiltinId::Postgres
3222 | BuiltinId::PostgresQuery => Err(runtime_err("Data operations not available in WASM")),
3223 #[cfg(feature = "native")]
3225 BuiltinId::Tensor => {
3226 if args.is_empty() {
3227 return Err(runtime_err("tensor() expects at least 1 argument"));
3228 }
3229 let data = self.vmvalue_to_f64_list(&args[0])?;
3230 let shape = if args.len() > 1 {
3231 self.vmvalue_to_usize_list(&args[1])?
3232 } else {
3233 vec![data.len()]
3234 };
3235 let t = tl_ai::TlTensor::from_vec(data, &shape)
3236 .map_err(|e| runtime_err(e.to_string()))?;
3237 Ok(VmValue::Tensor(Arc::new(t)))
3238 }
3239 #[cfg(feature = "native")]
3240 BuiltinId::TensorZeros => {
3241 if args.is_empty() {
3242 return Err(runtime_err("tensor_zeros() expects 1 argument (shape)"));
3243 }
3244 let shape = self.vmvalue_to_usize_list(&args[0])?;
3245 let t = tl_ai::TlTensor::zeros(&shape);
3246 Ok(VmValue::Tensor(Arc::new(t)))
3247 }
3248 #[cfg(feature = "native")]
3249 BuiltinId::TensorOnes => {
3250 if args.is_empty() {
3251 return Err(runtime_err("tensor_ones() expects 1 argument (shape)"));
3252 }
3253 let shape = self.vmvalue_to_usize_list(&args[0])?;
3254 let t = tl_ai::TlTensor::ones(&shape);
3255 Ok(VmValue::Tensor(Arc::new(t)))
3256 }
3257 #[cfg(feature = "native")]
3258 BuiltinId::TensorShape => match args.first() {
3259 Some(VmValue::Tensor(t)) => {
3260 let shape: Vec<VmValue> =
3261 t.shape().iter().map(|&d| VmValue::Int(d as i64)).collect();
3262 Ok(VmValue::List(Box::new(shape)))
3263 }
3264 _ => Err(runtime_err("tensor_shape() expects a tensor")),
3265 },
3266 #[cfg(feature = "native")]
3267 BuiltinId::TensorReshape => {
3268 if args.len() != 2 {
3269 return Err(runtime_err(
3270 "tensor_reshape() expects 2 arguments (tensor, shape)",
3271 ));
3272 }
3273 let t = match &args[0] {
3274 VmValue::Tensor(t) => (**t).clone(),
3275 _ => return Err(runtime_err("tensor_reshape() first arg must be a tensor")),
3276 };
3277 let shape = self.vmvalue_to_usize_list(&args[1])?;
3278 let reshaped = t.reshape(&shape).map_err(|e| runtime_err(e.to_string()))?;
3279 Ok(VmValue::Tensor(Arc::new(reshaped)))
3280 }
3281 #[cfg(feature = "native")]
3282 BuiltinId::TensorTranspose => match args.first() {
3283 Some(VmValue::Tensor(t)) => {
3284 let transposed = t.transpose().map_err(|e| runtime_err(e.to_string()))?;
3285 Ok(VmValue::Tensor(Arc::new(transposed)))
3286 }
3287 _ => Err(runtime_err("tensor_transpose() expects a tensor")),
3288 },
3289 #[cfg(feature = "native")]
3290 BuiltinId::TensorSum => match args.first() {
3291 Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.sum())),
3292 _ => Err(runtime_err("tensor_sum() expects a tensor")),
3293 },
3294 #[cfg(feature = "native")]
3295 BuiltinId::TensorMean => match args.first() {
3296 Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.mean())),
3297 _ => Err(runtime_err("tensor_mean() expects a tensor")),
3298 },
3299 #[cfg(feature = "native")]
3300 BuiltinId::TensorDot => {
3301 if args.len() != 2 {
3302 return Err(runtime_err("tensor_dot() expects 2 arguments"));
3303 }
3304 let a_t = match &args[0] {
3305 VmValue::Tensor(t) => t,
3306 _ => return Err(runtime_err("tensor_dot() first arg must be a tensor")),
3307 };
3308 let b_t = match &args[1] {
3309 VmValue::Tensor(t) => t,
3310 _ => return Err(runtime_err("tensor_dot() second arg must be a tensor")),
3311 };
3312 let result = a_t.dot(b_t).map_err(|e| runtime_err(e.to_string()))?;
3313 Ok(VmValue::Tensor(Arc::new(result)))
3314 }
3315 #[cfg(feature = "native")]
3316 BuiltinId::Predict => {
3317 if args.len() < 2 {
3318 return Err(runtime_err(
3319 "predict() expects at least 2 arguments (model, input)",
3320 ));
3321 }
3322 let model = match &args[0] {
3323 VmValue::Model(m) => (**m).clone(),
3324 _ => return Err(runtime_err("predict() first arg must be a model")),
3325 };
3326 let input = match &args[1] {
3327 VmValue::Tensor(t) => (**t).clone(),
3328 _ => return Err(runtime_err("predict() second arg must be a tensor")),
3329 };
3330 let result =
3331 tl_ai::predict(&model, &input).map_err(|e| runtime_err(e.to_string()))?;
3332 Ok(VmValue::Tensor(Arc::new(result)))
3333 }
3334 #[cfg(feature = "native")]
3335 BuiltinId::Similarity => {
3336 if args.len() != 2 {
3337 return Err(runtime_err("similarity() expects 2 arguments"));
3338 }
3339 let a_t = match &args[0] {
3340 VmValue::Tensor(t) => t,
3341 _ => return Err(runtime_err("similarity() first arg must be a tensor")),
3342 };
3343 let b_t = match &args[1] {
3344 VmValue::Tensor(t) => t,
3345 _ => return Err(runtime_err("similarity() second arg must be a tensor")),
3346 };
3347 let sim = tl_ai::similarity(a_t, b_t).map_err(|e| runtime_err(e.to_string()))?;
3348 Ok(VmValue::Float(sim))
3349 }
3350 #[cfg(feature = "native")]
3351 BuiltinId::AiComplete => {
3352 if args.is_empty() {
3353 return Err(runtime_err(
3354 "ai_complete() expects at least 1 argument (prompt)",
3355 ));
3356 }
3357 let prompt = match &args[0] {
3358 VmValue::String(s) => s.to_string(),
3359 _ => return Err(runtime_err("ai_complete() first arg must be a string")),
3360 };
3361 let model = match args.get(1) {
3362 Some(VmValue::String(s)) => Some(s.to_string()),
3363 _ => None,
3364 };
3365 let result = tl_ai::ai_complete(&prompt, model.as_deref(), None, None)
3366 .map_err(|e| runtime_err(e.to_string()))?;
3367 Ok(VmValue::String(Arc::from(result.as_str())))
3368 }
3369 #[cfg(feature = "native")]
3370 BuiltinId::AiChat => {
3371 if args.is_empty() {
3372 return Err(runtime_err("ai_chat() expects at least 1 argument (model)"));
3373 }
3374 let model = match &args[0] {
3375 VmValue::String(s) => s.to_string(),
3376 _ => return Err(runtime_err("ai_chat() first arg must be a string (model)")),
3377 };
3378 let system = match args.get(1) {
3379 Some(VmValue::String(s)) => Some(s.to_string()),
3380 _ => None,
3381 };
3382 let messages: Vec<(String, String)> = if let Some(VmValue::List(msgs)) = args.get(2)
3383 {
3384 msgs.chunks(2)
3385 .filter_map(|chunk| {
3386 if chunk.len() == 2
3387 && let (VmValue::String(role), VmValue::String(content)) =
3388 (&chunk[0], &chunk[1])
3389 {
3390 return Some((role.to_string(), content.to_string()));
3391 }
3392 None
3393 })
3394 .collect()
3395 } else {
3396 Vec::new()
3397 };
3398 let result = tl_ai::ai_chat(&model, system.as_deref(), &messages)
3399 .map_err(|e| runtime_err(e.to_string()))?;
3400 Ok(VmValue::String(Arc::from(result.as_str())))
3401 }
3402 #[cfg(feature = "native")]
3403 BuiltinId::ModelSave => {
3404 if args.len() != 2 {
3405 return Err(runtime_err(
3406 "model_save() expects 2 arguments (model, path)",
3407 ));
3408 }
3409 let model = match &args[0] {
3410 VmValue::Model(m) => m,
3411 _ => return Err(runtime_err("model_save() first arg must be a model")),
3412 };
3413 let path = match &args[1] {
3414 VmValue::String(s) => s.to_string(),
3415 _ => return Err(runtime_err("model_save() second arg must be a string path")),
3416 };
3417 model
3418 .save(std::path::Path::new(&path))
3419 .map_err(|e| runtime_err(e.to_string()))?;
3420 Ok(VmValue::None)
3421 }
3422 #[cfg(feature = "native")]
3423 BuiltinId::ModelLoad => {
3424 if args.is_empty() {
3425 return Err(runtime_err("model_load() expects 1 argument (path)"));
3426 }
3427 let path = match &args[0] {
3428 VmValue::String(s) => s.to_string(),
3429 _ => return Err(runtime_err("model_load() arg must be a string path")),
3430 };
3431 let model = tl_ai::TlModel::load(std::path::Path::new(&path))
3432 .map_err(|e| runtime_err(e.to_string()))?;
3433 Ok(VmValue::Model(Arc::new(model)))
3434 }
3435 #[cfg(feature = "native")]
3436 BuiltinId::ModelRegister => {
3437 if args.len() != 2 {
3438 return Err(runtime_err(
3439 "model_register() expects 2 arguments (name, model)",
3440 ));
3441 }
3442 let name = match &args[0] {
3443 VmValue::String(s) => s.to_string(),
3444 _ => return Err(runtime_err("model_register() first arg must be a string")),
3445 };
3446 let model = match &args[1] {
3447 VmValue::Model(m) => (**m).clone(),
3448 _ => return Err(runtime_err("model_register() second arg must be a model")),
3449 };
3450 let registry = tl_ai::ModelRegistry::default_location();
3451 registry
3452 .register(&name, &model)
3453 .map_err(|e| runtime_err(e.to_string()))?;
3454 Ok(VmValue::None)
3455 }
3456 #[cfg(feature = "native")]
3457 BuiltinId::ModelList => {
3458 let registry = tl_ai::ModelRegistry::default_location();
3459 let names = registry.list();
3460 let items: Vec<VmValue> = names
3461 .into_iter()
3462 .map(|n: String| VmValue::String(Arc::from(n.as_str())))
3463 .collect();
3464 Ok(VmValue::List(Box::new(items)))
3465 }
3466 #[cfg(feature = "native")]
3467 BuiltinId::ModelGet => {
3468 if args.is_empty() {
3469 return Err(runtime_err("model_get() expects 1 argument (name)"));
3470 }
3471 let name = match &args[0] {
3472 VmValue::String(s) => s.to_string(),
3473 _ => return Err(runtime_err("model_get() arg must be a string")),
3474 };
3475 let registry = tl_ai::ModelRegistry::default_location();
3476 match registry.get(&name) {
3477 Ok(m) => Ok(VmValue::Model(Arc::new(m))),
3478 Err(_) => Ok(VmValue::None),
3479 }
3480 }
3481 #[cfg(not(feature = "native"))]
3482 BuiltinId::Tensor
3483 | BuiltinId::TensorZeros
3484 | BuiltinId::TensorOnes
3485 | BuiltinId::TensorShape
3486 | BuiltinId::TensorReshape
3487 | BuiltinId::TensorTranspose
3488 | BuiltinId::TensorSum
3489 | BuiltinId::TensorMean
3490 | BuiltinId::TensorDot
3491 | BuiltinId::Predict
3492 | BuiltinId::Similarity
3493 | BuiltinId::AiComplete
3494 | BuiltinId::AiChat
3495 | BuiltinId::ModelSave
3496 | BuiltinId::ModelLoad
3497 | BuiltinId::ModelRegister
3498 | BuiltinId::ModelList
3499 | BuiltinId::ModelGet => Err(runtime_err("AI/ML operations not available in WASM")),
3500 #[cfg(feature = "native")]
3502 BuiltinId::AlertSlack => {
3503 if args.len() < 2 {
3504 return Err(runtime_err("alert_slack(url, msg) requires 2 args"));
3505 }
3506 let url = match &args[0] {
3507 VmValue::String(s) => s.to_string(),
3508 _ => return Err(runtime_err("alert_slack: url must be a string")),
3509 };
3510 let msg = format!("{}", args[1]);
3511 tl_stream::send_alert(&tl_stream::AlertTarget::Slack(url), &msg)
3512 .map_err(|e| runtime_err(&e))?;
3513 Ok(VmValue::None)
3514 }
3515 #[cfg(feature = "native")]
3516 BuiltinId::AlertWebhook => {
3517 if args.len() < 2 {
3518 return Err(runtime_err("alert_webhook(url, msg) requires 2 args"));
3519 }
3520 let url = match &args[0] {
3521 VmValue::String(s) => s.to_string(),
3522 _ => return Err(runtime_err("alert_webhook: url must be a string")),
3523 };
3524 let msg = format!("{}", args[1]);
3525 tl_stream::send_alert(&tl_stream::AlertTarget::Webhook(url), &msg)
3526 .map_err(|e| runtime_err(&e))?;
3527 Ok(VmValue::None)
3528 }
3529 #[cfg(feature = "native")]
3530 BuiltinId::Emit => {
3531 if args.is_empty() {
3532 return Err(runtime_err("emit() requires at least 1 argument"));
3533 }
3534 self.output.push(format!("emit: {}", args[0]));
3535 Ok(args[0].clone())
3536 }
3537 #[cfg(feature = "native")]
3538 BuiltinId::Lineage => Ok(VmValue::String(Arc::from("lineage_tracker"))),
3539 #[cfg(feature = "native")]
3540 BuiltinId::RunPipeline => {
3541 if args.is_empty() {
3542 return Err(runtime_err("run_pipeline() requires a pipeline"));
3543 }
3544 if let VmValue::PipelineDef(ref def) = args[0] {
3545 Ok(VmValue::String(Arc::from(
3546 format!("Pipeline '{}' triggered", def.name).as_str(),
3547 )))
3548 } else {
3549 Err(runtime_err("run_pipeline: argument must be a pipeline"))
3550 }
3551 }
3552 #[cfg(not(feature = "native"))]
3553 BuiltinId::AlertSlack
3554 | BuiltinId::AlertWebhook
3555 | BuiltinId::Emit
3556 | BuiltinId::Lineage
3557 | BuiltinId::RunPipeline => Err(runtime_err("Streaming not available in WASM")),
3558 BuiltinId::Sqrt => match args.first() {
3560 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sqrt())),
3561 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sqrt())),
3562 _ => Err(runtime_err("sqrt() expects a number")),
3563 },
3564 BuiltinId::Pow => {
3565 if args.len() == 2 {
3566 match (&args[0], &args[1]) {
3567 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
3568 (VmValue::Int(a), VmValue::Int(b)) => {
3569 Ok(VmValue::Float((*a as f64).powf(*b as f64)))
3570 }
3571 (VmValue::Float(a), VmValue::Int(b)) => {
3572 Ok(VmValue::Float(a.powf(*b as f64)))
3573 }
3574 (VmValue::Int(a), VmValue::Float(b)) => {
3575 Ok(VmValue::Float((*a as f64).powf(*b)))
3576 }
3577 _ => Err(runtime_err("pow() expects two numbers")),
3578 }
3579 } else {
3580 Err(runtime_err("pow() expects 2 arguments"))
3581 }
3582 }
3583 BuiltinId::Floor => match args.first() {
3584 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.floor())),
3585 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3586 _ => Err(runtime_err("floor() expects a number")),
3587 },
3588 BuiltinId::Ceil => match args.first() {
3589 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ceil())),
3590 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3591 _ => Err(runtime_err("ceil() expects a number")),
3592 },
3593 BuiltinId::Round => match args.first() {
3594 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.round())),
3595 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3596 _ => Err(runtime_err("round() expects a number")),
3597 },
3598 BuiltinId::Sin => match args.first() {
3599 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sin())),
3600 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sin())),
3601 _ => Err(runtime_err("sin() expects a number")),
3602 },
3603 BuiltinId::Cos => match args.first() {
3604 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.cos())),
3605 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).cos())),
3606 _ => Err(runtime_err("cos() expects a number")),
3607 },
3608 BuiltinId::Tan => match args.first() {
3609 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.tan())),
3610 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).tan())),
3611 _ => Err(runtime_err("tan() expects a number")),
3612 },
3613 BuiltinId::Log => match args.first() {
3614 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ln())),
3615 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).ln())),
3616 _ => Err(runtime_err("log() expects a number")),
3617 },
3618 BuiltinId::Log2 => match args.first() {
3619 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log2())),
3620 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log2())),
3621 _ => Err(runtime_err("log2() expects a number")),
3622 },
3623 BuiltinId::Log10 => match args.first() {
3624 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log10())),
3625 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log10())),
3626 _ => Err(runtime_err("log10() expects a number")),
3627 },
3628 BuiltinId::Join => {
3629 if args.len() == 2 {
3630 if let (VmValue::String(sep), VmValue::List(items)) = (&args[0], &args[1]) {
3631 let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
3632 Ok(VmValue::String(Arc::from(
3633 parts.join(sep.as_ref()).as_str(),
3634 )))
3635 } else {
3636 Err(runtime_err("join() expects separator and list"))
3637 }
3638 } else {
3639 Err(runtime_err("join() expects 2 arguments"))
3640 }
3641 }
3642 #[cfg(feature = "native")]
3643 BuiltinId::HttpGet => {
3644 self.check_permission("network")?;
3645 if args.is_empty() {
3646 return Err(runtime_err("http_get() expects a URL"));
3647 }
3648 if let VmValue::String(url) = &args[0] {
3649 match reqwest::blocking::get(url.as_ref()).and_then(|r| r.text()) {
3650 Ok(body) => Ok(VmValue::String(Arc::from(body.as_str()))),
3651 Err(e) => {
3652 let msg = format!("HTTP GET error: {e}");
3653 self.thrown_value =
3654 Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3655 type_name: Arc::from("NetworkError"),
3656 variant: Arc::from("HttpError"),
3657 fields: vec![
3658 VmValue::String(Arc::from(msg.as_str())),
3659 VmValue::String(url.clone()),
3660 ],
3661 })));
3662 Err(runtime_err(msg))
3663 }
3664 }
3665 } else {
3666 Err(runtime_err("http_get() expects a string URL"))
3667 }
3668 }
3669 #[cfg(feature = "native")]
3670 BuiltinId::HttpPost => {
3671 self.check_permission("network")?;
3672 if args.len() < 2 {
3673 return Err(runtime_err("http_post() expects URL and body"));
3674 }
3675 if let (VmValue::String(url), VmValue::String(body)) = (&args[0], &args[1]) {
3676 let client = reqwest::blocking::Client::new();
3677 match client
3678 .post(url.as_ref())
3679 .header("Content-Type", "application/json")
3680 .body(body.to_string())
3681 .send()
3682 .and_then(|r| r.text())
3683 {
3684 Ok(resp) => Ok(VmValue::String(Arc::from(resp.as_str()))),
3685 Err(e) => {
3686 let msg = format!("HTTP POST error: {e}");
3687 self.thrown_value =
3688 Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3689 type_name: Arc::from("NetworkError"),
3690 variant: Arc::from("HttpError"),
3691 fields: vec![
3692 VmValue::String(Arc::from(msg.as_str())),
3693 VmValue::String(url.clone()),
3694 ],
3695 })));
3696 Err(runtime_err(msg))
3697 }
3698 }
3699 } else {
3700 Err(runtime_err("http_post() expects string URL and body"))
3701 }
3702 }
3703 #[cfg(not(feature = "native"))]
3704 BuiltinId::HttpGet | BuiltinId::HttpPost => {
3705 Err(runtime_err("HTTP requests not available in WASM"))
3706 }
3707 BuiltinId::Assert => {
3708 if args.is_empty() {
3709 return Err(runtime_err("assert() expects at least 1 argument"));
3710 }
3711 if !args[0].is_truthy() {
3712 let msg = if args.len() > 1 {
3713 format!("{}", args[1])
3714 } else {
3715 "Assertion failed".to_string()
3716 };
3717 Err(runtime_err(msg))
3718 } else {
3719 Ok(VmValue::None)
3720 }
3721 }
3722 BuiltinId::AssertEq => {
3723 if args.len() < 2 {
3724 return Err(runtime_err("assert_eq() expects 2 arguments"));
3725 }
3726 let eq = match (&args[0], &args[1]) {
3727 (VmValue::Int(a), VmValue::Int(b)) => a == b,
3728 (VmValue::Float(a), VmValue::Float(b)) => a == b,
3729 (VmValue::String(a), VmValue::String(b)) => a == b,
3730 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
3731 (VmValue::None, VmValue::None) => true,
3732 _ => false,
3733 };
3734 if !eq {
3735 Err(runtime_err(format!(
3736 "Assertion failed: {} != {}",
3737 args[0], args[1]
3738 )))
3739 } else {
3740 Ok(VmValue::None)
3741 }
3742 }
3743 BuiltinId::JsonParse => {
3745 if args.is_empty() {
3746 return Err(runtime_err("json_parse() expects a string"));
3747 }
3748 if let VmValue::String(s) = &args[0] {
3749 let json_val: serde_json::Value = serde_json::from_str(s)
3750 .map_err(|e| runtime_err(format!("JSON parse error: {e}")))?;
3751 Ok(vm_json_to_value(&json_val))
3752 } else {
3753 Err(runtime_err("json_parse() expects a string"))
3754 }
3755 }
3756 BuiltinId::JsonStringify => {
3757 if args.is_empty() {
3758 return Err(runtime_err("json_stringify() expects a value"));
3759 }
3760 let json = vm_value_to_json(&args[0]);
3761 Ok(VmValue::String(Arc::from(json.to_string().as_str())))
3762 }
3763 BuiltinId::MapFrom => {
3764 if !args.len().is_multiple_of(2) {
3765 return Err(runtime_err(
3766 "map_from() expects even number of arguments (key, value pairs)",
3767 ));
3768 }
3769 let mut pairs = Vec::new();
3770 for chunk in args.chunks(2) {
3771 let key = match &chunk[0] {
3772 VmValue::String(s) => s.clone(),
3773 other => Arc::from(format!("{other}").as_str()),
3774 };
3775 pairs.push((key, chunk[1].clone()));
3776 }
3777 Ok(VmValue::Map(Box::new(pairs)))
3778 }
3779 #[cfg(feature = "native")]
3780 BuiltinId::ReadFile => {
3781 self.check_permission("file_read")?;
3782 if args.is_empty() {
3783 return Err(runtime_err("read_file() expects a path"));
3784 }
3785 if let VmValue::String(path) = &args[0] {
3786 let content = std::fs::read_to_string(path.as_ref())
3787 .map_err(|e| runtime_err(format!("read_file error: {e}")))?;
3788 Ok(VmValue::String(Arc::from(content.as_str())))
3789 } else {
3790 Err(runtime_err("read_file() expects a string path"))
3791 }
3792 }
3793 #[cfg(feature = "native")]
3794 BuiltinId::WriteFile => {
3795 self.check_permission("file_write")?;
3796 if args.len() < 2 {
3797 return Err(runtime_err("write_file() expects path and content"));
3798 }
3799 if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3800 std::fs::write(path.as_ref(), content.as_ref())
3801 .map_err(|e| runtime_err(format!("write_file error: {e}")))?;
3802 Ok(VmValue::None)
3803 } else {
3804 Err(runtime_err("write_file() expects string path and content"))
3805 }
3806 }
3807 #[cfg(feature = "native")]
3808 BuiltinId::AppendFile => {
3809 self.check_permission("file_write")?;
3810 if args.len() < 2 {
3811 return Err(runtime_err("append_file() expects path and content"));
3812 }
3813 if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3814 use std::io::Write;
3815 let mut file = std::fs::OpenOptions::new()
3816 .create(true)
3817 .append(true)
3818 .open(path.as_ref())
3819 .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3820 file.write_all(content.as_bytes())
3821 .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3822 Ok(VmValue::None)
3823 } else {
3824 Err(runtime_err("append_file() expects string path and content"))
3825 }
3826 }
3827 #[cfg(feature = "native")]
3828 BuiltinId::FileExists => {
3829 self.check_permission("file_read")?;
3830 if args.is_empty() {
3831 return Err(runtime_err("file_exists() expects a path"));
3832 }
3833 if let VmValue::String(path) = &args[0] {
3834 Ok(VmValue::Bool(std::path::Path::new(path.as_ref()).exists()))
3835 } else {
3836 Err(runtime_err("file_exists() expects a string path"))
3837 }
3838 }
3839 #[cfg(feature = "native")]
3840 BuiltinId::ListDir => {
3841 self.check_permission("file_read")?;
3842 if args.is_empty() {
3843 return Err(runtime_err("list_dir() expects a path"));
3844 }
3845 if let VmValue::String(path) = &args[0] {
3846 let entries: Vec<VmValue> = std::fs::read_dir(path.as_ref())
3847 .map_err(|e| runtime_err(format!("list_dir error: {e}")))?
3848 .filter_map(|e| e.ok())
3849 .map(|e| {
3850 VmValue::String(Arc::from(e.file_name().to_string_lossy().as_ref()))
3851 })
3852 .collect();
3853 Ok(VmValue::List(Box::new(entries)))
3854 } else {
3855 Err(runtime_err("list_dir() expects a string path"))
3856 }
3857 }
3858 #[cfg(not(feature = "native"))]
3859 BuiltinId::ReadFile
3860 | BuiltinId::WriteFile
3861 | BuiltinId::AppendFile
3862 | BuiltinId::FileExists
3863 | BuiltinId::ListDir => Err(runtime_err("File I/O not available in WASM")),
3864 #[cfg(feature = "native")]
3865 BuiltinId::EnvGet => {
3866 if args.is_empty() {
3867 return Err(runtime_err("env_get() expects a name"));
3868 }
3869 if let VmValue::String(name) = &args[0] {
3870 match std::env::var(name.as_ref()) {
3871 Ok(val) => Ok(VmValue::String(Arc::from(val.as_str()))),
3872 Err(_) => Ok(VmValue::None),
3873 }
3874 } else {
3875 Err(runtime_err("env_get() expects a string"))
3876 }
3877 }
3878 #[cfg(feature = "native")]
3879 BuiltinId::EnvSet => {
3880 self.check_permission("env_write")?;
3881 if args.len() < 2 {
3882 return Err(runtime_err("env_set() expects name and value"));
3883 }
3884 if let (VmValue::String(name), VmValue::String(val)) = (&args[0], &args[1]) {
3885 let _guard = env_lock();
3886 unsafe {
3887 std::env::set_var(name.as_ref(), val.as_ref());
3888 }
3889 Ok(VmValue::None)
3890 } else {
3891 Err(runtime_err("env_set() expects two strings"))
3892 }
3893 }
3894 #[cfg(not(feature = "native"))]
3895 BuiltinId::EnvGet | BuiltinId::EnvSet => {
3896 Err(runtime_err("Environment variables not available in WASM"))
3897 }
3898 BuiltinId::RegexMatch => {
3899 if args.len() < 2 {
3900 return Err(runtime_err("regex_match() expects pattern and string"));
3901 }
3902 if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3903 if pattern.len() > 10_000 {
3904 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3905 }
3906 let re = regex::RegexBuilder::new(pattern)
3907 .size_limit(10_000_000)
3908 .build()
3909 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3910 Ok(VmValue::Bool(re.is_match(text)))
3911 } else {
3912 Err(runtime_err(
3913 "regex_match() expects string pattern and string",
3914 ))
3915 }
3916 }
3917 BuiltinId::RegexFind => {
3918 if args.len() < 2 {
3919 return Err(runtime_err("regex_find() expects pattern and string"));
3920 }
3921 if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3922 if pattern.len() > 10_000 {
3923 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3924 }
3925 let re = regex::RegexBuilder::new(pattern)
3926 .size_limit(10_000_000)
3927 .build()
3928 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3929 let matches: Vec<VmValue> = re
3930 .find_iter(text)
3931 .map(|m| VmValue::String(Arc::from(m.as_str())))
3932 .collect();
3933 Ok(VmValue::List(Box::new(matches)))
3934 } else {
3935 Err(runtime_err(
3936 "regex_find() expects string pattern and string",
3937 ))
3938 }
3939 }
3940 BuiltinId::RegexReplace => {
3941 if args.len() < 3 {
3942 return Err(runtime_err(
3943 "regex_replace() expects pattern, string, replacement",
3944 ));
3945 }
3946 if let (
3947 VmValue::String(pattern),
3948 VmValue::String(text),
3949 VmValue::String(replacement),
3950 ) = (&args[0], &args[1], &args[2])
3951 {
3952 if pattern.len() > 10_000 {
3953 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3954 }
3955 let re = regex::RegexBuilder::new(pattern)
3956 .size_limit(10_000_000)
3957 .build()
3958 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3959 Ok(VmValue::String(Arc::from(
3960 re.replace_all(text, replacement.as_ref()).as_ref(),
3961 )))
3962 } else {
3963 Err(runtime_err("regex_replace() expects three strings"))
3964 }
3965 }
3966 BuiltinId::Now => {
3967 let ts = chrono::Utc::now().timestamp_millis();
3968 Ok(VmValue::DateTime(ts))
3969 }
3970 BuiltinId::DateFormat => {
3971 if args.len() < 2 {
3972 return Err(runtime_err(
3973 "date_format() expects datetime/timestamp and format",
3974 ));
3975 }
3976 let ts = match &args[0] {
3977 VmValue::DateTime(ms) => *ms,
3978 VmValue::Int(ms) => *ms,
3979 _ => {
3980 return Err(runtime_err(
3981 "date_format() expects a datetime or int timestamp",
3982 ));
3983 }
3984 };
3985 let fmt = match &args[1] {
3986 VmValue::String(s) => s,
3987 _ => return Err(runtime_err("date_format() expects a string format")),
3988 };
3989 use chrono::TimeZone;
3990 let secs = ts / 1000;
3991 let nsecs = ((ts % 1000) * 1_000_000) as u32;
3992 let dt = chrono::Utc
3993 .timestamp_opt(secs, nsecs)
3994 .single()
3995 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
3996 Ok(VmValue::String(Arc::from(
3997 dt.format(fmt.as_ref()).to_string().as_str(),
3998 )))
3999 }
4000 BuiltinId::DateParse => {
4001 if args.len() < 2 {
4002 return Err(runtime_err("date_parse() expects string and format"));
4003 }
4004 if let (VmValue::String(s), VmValue::String(fmt)) = (&args[0], &args[1]) {
4005 let dt = chrono::NaiveDateTime::parse_from_str(s, fmt)
4006 .map_err(|e| runtime_err(format!("date_parse error: {e}")))?;
4007 let ts = dt.and_utc().timestamp_millis();
4008 Ok(VmValue::DateTime(ts))
4009 } else {
4010 Err(runtime_err("date_parse() expects two strings"))
4011 }
4012 }
4013 BuiltinId::Zip => {
4014 if args.len() < 2 {
4015 return Err(runtime_err("zip() expects two lists"));
4016 }
4017 if let (VmValue::List(a), VmValue::List(b)) = (&args[0], &args[1]) {
4018 let pairs: Vec<VmValue> = a
4019 .iter()
4020 .zip(b.iter())
4021 .map(|(x, y)| VmValue::List(Box::new(vec![x.clone(), y.clone()])))
4022 .collect();
4023 Ok(VmValue::List(Box::new(pairs)))
4024 } else {
4025 Err(runtime_err("zip() expects two lists"))
4026 }
4027 }
4028 BuiltinId::Enumerate => {
4029 if args.is_empty() {
4030 return Err(runtime_err("enumerate() expects a list"));
4031 }
4032 if let VmValue::List(items) = &args[0] {
4033 let pairs: Vec<VmValue> = items
4034 .iter()
4035 .enumerate()
4036 .map(|(i, v)| {
4037 VmValue::List(Box::new(vec![VmValue::Int(i as i64), v.clone()]))
4038 })
4039 .collect();
4040 Ok(VmValue::List(Box::new(pairs)))
4041 } else {
4042 Err(runtime_err("enumerate() expects a list"))
4043 }
4044 }
4045 BuiltinId::Bool => {
4046 if args.is_empty() {
4047 return Err(runtime_err("bool() expects a value"));
4048 }
4049 Ok(VmValue::Bool(args[0].is_truthy()))
4050 }
4051
4052 #[cfg(feature = "native")]
4054 BuiltinId::Spawn => {
4055 if args.is_empty() {
4056 return Err(runtime_err("spawn() expects a function argument"));
4057 }
4058 match &args[0] {
4059 VmValue::Function(closure) => {
4060 let proto = closure.prototype.clone();
4061 let mut closed_upvalues = Vec::new();
4063 for uv in &closure.upvalues {
4064 match uv {
4065 UpvalueRef::Open { stack_index } => {
4066 let val = self.stack[*stack_index].clone();
4067 closed_upvalues.push(UpvalueRef::Closed(val));
4068 }
4069 UpvalueRef::Closed(v) => {
4070 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4071 }
4072 }
4073 }
4074 let globals = self.globals.clone();
4075 let (tx, rx) = mpsc::channel::<Result<VmValue, String>>();
4076
4077 std::thread::spawn(move || {
4078 let mut vm = Vm::new();
4079 vm.globals = globals;
4080 let result = vm.execute_closure(&proto, &closed_upvalues);
4081 let _ = tx.send(result.map_err(|e| match e {
4082 TlError::Runtime(re) => re.message,
4083 other => format!("{other}"),
4084 }));
4085 });
4086
4087 Ok(VmValue::Task(Arc::new(VmTask::new(rx))))
4088 }
4089 _ => Err(runtime_err("spawn() expects a function")),
4090 }
4091 }
4092 #[cfg(feature = "native")]
4093 BuiltinId::Sleep => {
4094 if args.is_empty() {
4095 return Err(runtime_err("sleep() expects a duration in milliseconds"));
4096 }
4097 match &args[0] {
4098 VmValue::Int(ms) => {
4099 std::thread::sleep(Duration::from_millis(*ms as u64));
4100 Ok(VmValue::None)
4101 }
4102 _ => Err(runtime_err("sleep() expects an integer (milliseconds)")),
4103 }
4104 }
4105 #[cfg(feature = "native")]
4106 BuiltinId::Channel => {
4107 let capacity = match args.first() {
4108 Some(VmValue::Int(n)) => *n as usize,
4109 None => 64,
4110 _ => {
4111 return Err(runtime_err(
4112 "channel() expects an optional integer capacity",
4113 ));
4114 }
4115 };
4116 Ok(VmValue::Channel(Arc::new(VmChannel::new(capacity))))
4117 }
4118 #[cfg(feature = "native")]
4119 BuiltinId::Send => {
4120 if args.len() < 2 {
4121 return Err(runtime_err("send() expects a channel and a value"));
4122 }
4123 match &args[0] {
4124 VmValue::Channel(ch) => {
4125 ch.sender
4126 .send(args[1].clone())
4127 .map_err(|_| runtime_err("Channel disconnected"))?;
4128 Ok(VmValue::None)
4129 }
4130 _ => Err(runtime_err("send() expects a channel as first argument")),
4131 }
4132 }
4133 #[cfg(feature = "native")]
4134 BuiltinId::Recv => {
4135 if args.is_empty() {
4136 return Err(runtime_err("recv() expects a channel"));
4137 }
4138 match &args[0] {
4139 VmValue::Channel(ch) => {
4140 let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4141 match guard.recv() {
4142 Ok(val) => Ok(val),
4143 Err(_) => Ok(VmValue::None),
4144 }
4145 }
4146 _ => Err(runtime_err("recv() expects a channel")),
4147 }
4148 }
4149 #[cfg(feature = "native")]
4150 BuiltinId::TryRecv => {
4151 if args.is_empty() {
4152 return Err(runtime_err("try_recv() expects a channel"));
4153 }
4154 match &args[0] {
4155 VmValue::Channel(ch) => {
4156 let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4157 match guard.try_recv() {
4158 Ok(val) => Ok(val),
4159 Err(_) => Ok(VmValue::None),
4160 }
4161 }
4162 _ => Err(runtime_err("try_recv() expects a channel")),
4163 }
4164 }
4165 #[cfg(feature = "native")]
4166 BuiltinId::AwaitAll => {
4167 if args.is_empty() {
4168 return Err(runtime_err("await_all() expects a list of tasks"));
4169 }
4170 match &args[0] {
4171 VmValue::List(tasks) => {
4172 let mut results = Vec::with_capacity(tasks.len());
4173 for task in tasks.iter() {
4174 match task {
4175 VmValue::Task(t) => {
4176 let rx = {
4177 let mut guard =
4178 t.receiver.lock().unwrap_or_else(|e| e.into_inner());
4179 guard.take()
4180 };
4181 match rx {
4182 Some(receiver) => match receiver.recv() {
4183 Ok(Ok(val)) => results.push(val),
4184 Ok(Err(e)) => return Err(runtime_err(e)),
4185 Err(_) => {
4186 return Err(runtime_err(
4187 "Task channel disconnected",
4188 ));
4189 }
4190 },
4191 None => return Err(runtime_err("Task already awaited")),
4192 }
4193 }
4194 other => results.push(other.clone()),
4195 }
4196 }
4197 Ok(VmValue::List(Box::new(results)))
4198 }
4199 _ => Err(runtime_err("await_all() expects a list")),
4200 }
4201 }
4202 #[cfg(feature = "native")]
4203 BuiltinId::Pmap => {
4204 if args.len() < 2 {
4205 return Err(runtime_err("pmap() expects a list and a function"));
4206 }
4207 let items = match &args[0] {
4208 VmValue::List(items) => (**items).clone(),
4209 _ => return Err(runtime_err("pmap() expects a list as first argument")),
4210 };
4211 let closure = match &args[1] {
4212 VmValue::Function(c) => c.clone(),
4213 _ => return Err(runtime_err("pmap() expects a function as second argument")),
4214 };
4215
4216 let mut closed_upvalues = Vec::new();
4218 for uv in &closure.upvalues {
4219 match uv {
4220 UpvalueRef::Open { stack_index } => {
4221 let val = self.stack[*stack_index].clone();
4222 closed_upvalues.push(UpvalueRef::Closed(val));
4223 }
4224 UpvalueRef::Closed(v) => {
4225 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4226 }
4227 }
4228 }
4229
4230 let proto = closure.prototype.clone();
4231 let globals = self.globals.clone();
4232
4233 let mut handles = Vec::with_capacity(items.len());
4235 for item in items {
4236 let proto = proto.clone();
4237 let upvalues = closed_upvalues.clone();
4238 let globals = globals.clone();
4239 let handle = std::thread::spawn(move || {
4240 let mut vm = Vm::new();
4241 vm.globals = globals;
4242 vm.execute_closure_with_args(&proto, &upvalues, &[item])
4243 .map_err(|e| match e {
4244 TlError::Runtime(re) => re.message,
4245 other => format!("{other}"),
4246 })
4247 });
4248 handles.push(handle);
4249 }
4250
4251 let mut results = Vec::with_capacity(handles.len());
4252 for handle in handles {
4253 match handle.join() {
4254 Ok(Ok(val)) => results.push(val),
4255 Ok(Err(e)) => return Err(runtime_err(e)),
4256 Err(_) => return Err(runtime_err("pmap() thread panicked")),
4257 }
4258 }
4259 Ok(VmValue::List(Box::new(results)))
4260 }
4261 #[cfg(feature = "native")]
4262 BuiltinId::Timeout => {
4263 if args.len() < 2 {
4264 return Err(runtime_err(
4265 "timeout() expects a task and a duration in milliseconds",
4266 ));
4267 }
4268 let ms = match &args[1] {
4269 VmValue::Int(n) => *n as u64,
4270 _ => return Err(runtime_err("timeout() expects an integer duration")),
4271 };
4272 match &args[0] {
4273 VmValue::Task(task) => {
4274 let rx = {
4275 let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
4276 guard.take()
4277 };
4278 match rx {
4279 Some(receiver) => {
4280 match receiver.recv_timeout(Duration::from_millis(ms)) {
4281 Ok(Ok(val)) => Ok(val),
4282 Ok(Err(e)) => Err(runtime_err(e)),
4283 Err(mpsc::RecvTimeoutError::Timeout) => {
4284 Err(runtime_err("Task timed out"))
4285 }
4286 Err(mpsc::RecvTimeoutError::Disconnected) => {
4287 Err(runtime_err("Task channel disconnected"))
4288 }
4289 }
4290 }
4291 None => Err(runtime_err("Task already awaited")),
4292 }
4293 }
4294 _ => Err(runtime_err("timeout() expects a task as first argument")),
4295 }
4296 }
4297 #[cfg(not(feature = "native"))]
4298 BuiltinId::Spawn
4299 | BuiltinId::Sleep
4300 | BuiltinId::Channel
4301 | BuiltinId::Send
4302 | BuiltinId::Recv
4303 | BuiltinId::TryRecv
4304 | BuiltinId::AwaitAll
4305 | BuiltinId::Pmap
4306 | BuiltinId::Timeout => Err(runtime_err("Threading not available in WASM")),
4307 BuiltinId::Next => {
4309 if args.is_empty() {
4310 return Err(runtime_err("next() expects a generator"));
4311 }
4312 match &args[0] {
4313 VmValue::Generator(gen_arc) => {
4314 let g = gen_arc.clone();
4315 self.generator_next(&g)
4316 }
4317 _ => Err(runtime_err("next() expects a generator")),
4318 }
4319 }
4320 BuiltinId::IsGenerator => {
4321 let val = args.first().unwrap_or(&VmValue::None);
4322 Ok(VmValue::Bool(matches!(val, VmValue::Generator(_))))
4323 }
4324 BuiltinId::Iter => {
4325 if args.is_empty() {
4326 return Err(runtime_err("iter() expects a list"));
4327 }
4328 match &args[0] {
4329 VmValue::List(items) => {
4330 let gn = VmGenerator::new(GeneratorKind::ListIter {
4331 items: (**items).clone(),
4332 index: 0,
4333 });
4334 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4335 }
4336 _ => Err(runtime_err("iter() expects a list")),
4337 }
4338 }
4339 BuiltinId::Take => {
4340 if args.len() < 2 {
4341 return Err(runtime_err("take() expects a generator and a count"));
4342 }
4343 let gen_arc = match &args[0] {
4344 VmValue::Generator(g) => g.clone(),
4345 _ => return Err(runtime_err("take() expects a generator as first argument")),
4346 };
4347 let n = match &args[1] {
4348 VmValue::Int(n) => *n as usize,
4349 _ => return Err(runtime_err("take() expects an integer count")),
4350 };
4351 let gn = VmGenerator::new(GeneratorKind::Take {
4352 source: gen_arc,
4353 remaining: n,
4354 });
4355 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4356 }
4357 BuiltinId::Skip_ => {
4358 if args.len() < 2 {
4359 return Err(runtime_err("skip() expects a generator and a count"));
4360 }
4361 let gen_arc = match &args[0] {
4362 VmValue::Generator(g) => g.clone(),
4363 _ => return Err(runtime_err("skip() expects a generator as first argument")),
4364 };
4365 let n = match &args[1] {
4366 VmValue::Int(n) => *n as usize,
4367 _ => return Err(runtime_err("skip() expects an integer count")),
4368 };
4369 let gn = VmGenerator::new(GeneratorKind::Skip {
4370 source: gen_arc,
4371 remaining: n,
4372 });
4373 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4374 }
4375 BuiltinId::GenCollect => {
4376 if args.is_empty() {
4377 return Err(runtime_err("gen_collect() expects a generator"));
4378 }
4379 match &args[0] {
4380 VmValue::Generator(gen_arc) => {
4381 let g = gen_arc.clone();
4382 let mut items = Vec::new();
4383 loop {
4384 let val = self.generator_next(&g)?;
4385 if matches!(val, VmValue::None) {
4386 break;
4387 }
4388 items.push(val);
4389 }
4390 Ok(VmValue::List(Box::new(items)))
4391 }
4392 _ => Err(runtime_err("gen_collect() expects a generator")),
4393 }
4394 }
4395 BuiltinId::GenMap => {
4396 if args.len() < 2 {
4397 return Err(runtime_err("gen_map() expects a generator and a function"));
4398 }
4399 let gen_arc = match &args[0] {
4400 VmValue::Generator(g) => g.clone(),
4401 _ => {
4402 return Err(runtime_err(
4403 "gen_map() expects a generator as first argument",
4404 ));
4405 }
4406 };
4407 let func = args[1].clone();
4408 let gn = VmGenerator::new(GeneratorKind::Map {
4409 source: gen_arc,
4410 func,
4411 });
4412 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4413 }
4414 BuiltinId::GenFilter => {
4415 if args.len() < 2 {
4416 return Err(runtime_err(
4417 "gen_filter() expects a generator and a function",
4418 ));
4419 }
4420 let gen_arc = match &args[0] {
4421 VmValue::Generator(g) => g.clone(),
4422 _ => {
4423 return Err(runtime_err(
4424 "gen_filter() expects a generator as first argument",
4425 ));
4426 }
4427 };
4428 let func = args[1].clone();
4429 let gn = VmGenerator::new(GeneratorKind::Filter {
4430 source: gen_arc,
4431 func,
4432 });
4433 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4434 }
4435 BuiltinId::Chain => {
4436 if args.len() < 2 {
4437 return Err(runtime_err("chain() expects two generators"));
4438 }
4439 let first = match &args[0] {
4440 VmValue::Generator(g) => g.clone(),
4441 _ => return Err(runtime_err("chain() expects generators")),
4442 };
4443 let second = match &args[1] {
4444 VmValue::Generator(g) => g.clone(),
4445 _ => return Err(runtime_err("chain() expects generators")),
4446 };
4447 let gn = VmGenerator::new(GeneratorKind::Chain {
4448 first,
4449 second,
4450 on_second: false,
4451 });
4452 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4453 }
4454 BuiltinId::GenZip => {
4455 if args.len() < 2 {
4456 return Err(runtime_err("gen_zip() expects two generators"));
4457 }
4458 let first = match &args[0] {
4459 VmValue::Generator(g) => g.clone(),
4460 _ => return Err(runtime_err("gen_zip() expects generators")),
4461 };
4462 let second = match &args[1] {
4463 VmValue::Generator(g) => g.clone(),
4464 _ => return Err(runtime_err("gen_zip() expects generators")),
4465 };
4466 let gn = VmGenerator::new(GeneratorKind::Zip { first, second });
4467 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4468 }
4469 BuiltinId::GenEnumerate => {
4470 if args.is_empty() {
4471 return Err(runtime_err("gen_enumerate() expects a generator"));
4472 }
4473 let gen_arc = match &args[0] {
4474 VmValue::Generator(g) => g.clone(),
4475 _ => return Err(runtime_err("gen_enumerate() expects a generator")),
4476 };
4477 let gn = VmGenerator::new(GeneratorKind::Enumerate {
4478 source: gen_arc,
4479 index: 0,
4480 });
4481 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4482 }
4483 BuiltinId::Ok => {
4485 let val = if args.is_empty() {
4486 VmValue::None
4487 } else {
4488 args[0].clone()
4489 };
4490 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4491 type_name: Arc::from("Result"),
4492 variant: Arc::from("Ok"),
4493 fields: vec![val],
4494 })))
4495 }
4496 BuiltinId::Err_ => {
4497 let val = if args.is_empty() {
4498 VmValue::String(Arc::from("error"))
4499 } else {
4500 args[0].clone()
4501 };
4502 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4503 type_name: Arc::from("Result"),
4504 variant: Arc::from("Err"),
4505 fields: vec![val],
4506 })))
4507 }
4508 BuiltinId::IsOk => {
4509 if args.is_empty() {
4510 return Err(runtime_err("is_ok() expects an argument"));
4511 }
4512 match &args[0] {
4513 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4514 Ok(VmValue::Bool(ei.variant.as_ref() == "Ok"))
4515 }
4516 _ => Ok(VmValue::Bool(false)),
4517 }
4518 }
4519 BuiltinId::IsErr => {
4520 if args.is_empty() {
4521 return Err(runtime_err("is_err() expects an argument"));
4522 }
4523 match &args[0] {
4524 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4525 Ok(VmValue::Bool(ei.variant.as_ref() == "Err"))
4526 }
4527 _ => Ok(VmValue::Bool(false)),
4528 }
4529 }
4530 BuiltinId::Unwrap => {
4531 if args.is_empty() {
4532 return Err(runtime_err("unwrap() expects an argument"));
4533 }
4534 match &args[0] {
4535 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4536 if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
4537 Ok(ei.fields[0].clone())
4538 } else if ei.variant.as_ref() == "Err" {
4539 let msg = if ei.fields.is_empty() {
4540 "error".to_string()
4541 } else {
4542 format!("{}", ei.fields[0])
4543 };
4544 Err(runtime_err(format!("unwrap() called on Err({msg})")))
4545 } else {
4546 Ok(VmValue::None)
4547 }
4548 }
4549 VmValue::None => Err(runtime_err("unwrap() called on none".to_string())),
4550 other => Ok(other.clone()),
4551 }
4552 }
4553 BuiltinId::SetFrom => {
4554 let list = match args.first() {
4555 Some(VmValue::List(items)) => items,
4556 _ => return Err(runtime_err("set_from() expects a list")),
4557 };
4558 if list.is_empty() {
4559 return Ok(VmValue::Set(Box::default()));
4560 }
4561 let mut result = Vec::new();
4562 for item in list.iter() {
4563 if !result.iter().any(|x| vm_values_equal(x, item)) {
4564 result.push(item.clone());
4565 }
4566 }
4567 Ok(VmValue::Set(Box::new(result)))
4568 }
4569 BuiltinId::SetAdd => {
4570 if args.len() < 2 {
4571 return Err(runtime_err("set_add() expects 2 arguments"));
4572 }
4573 let val = &args[1];
4574 match &args[0] {
4575 VmValue::Set(items) => {
4576 let mut new_items = items.clone();
4577 if !new_items.iter().any(|x| vm_values_equal(x, val)) {
4578 new_items.push(val.clone());
4579 }
4580 Ok(VmValue::Set(new_items))
4581 }
4582 _ => Err(runtime_err("set_add() first argument must be a set")),
4583 }
4584 }
4585 BuiltinId::SetRemove => {
4586 if args.len() < 2 {
4587 return Err(runtime_err("set_remove() expects 2 arguments"));
4588 }
4589 let val = &args[1];
4590 match &args[0] {
4591 VmValue::Set(items) => {
4592 let new_items: Vec<VmValue> = items
4593 .iter()
4594 .filter(|x| !vm_values_equal(x, val))
4595 .cloned()
4596 .collect();
4597 Ok(VmValue::Set(Box::new(new_items)))
4598 }
4599 _ => Err(runtime_err("set_remove() first argument must be a set")),
4600 }
4601 }
4602 BuiltinId::SetContains => {
4603 if args.len() < 2 {
4604 return Err(runtime_err("set_contains() expects 2 arguments"));
4605 }
4606 let val = &args[1];
4607 match &args[0] {
4608 VmValue::Set(items) => {
4609 Ok(VmValue::Bool(items.iter().any(|x| vm_values_equal(x, val))))
4610 }
4611 _ => Err(runtime_err("set_contains() first argument must be a set")),
4612 }
4613 }
4614 BuiltinId::SetUnion => {
4615 if args.len() < 2 {
4616 return Err(runtime_err("set_union() expects 2 arguments"));
4617 }
4618 match (&args[0], &args[1]) {
4619 (VmValue::Set(a), VmValue::Set(b)) => {
4620 let mut result = a.clone();
4621 for item in b.iter() {
4622 if !result.iter().any(|x| vm_values_equal(x, item)) {
4623 result.push(item.clone());
4624 }
4625 }
4626 Ok(VmValue::Set(result))
4627 }
4628 _ => Err(runtime_err("set_union() expects two sets")),
4629 }
4630 }
4631 BuiltinId::SetIntersection => {
4632 if args.len() < 2 {
4633 return Err(runtime_err("set_intersection() expects 2 arguments"));
4634 }
4635 match (&args[0], &args[1]) {
4636 (VmValue::Set(a), VmValue::Set(b)) => {
4637 let result: Vec<VmValue> = a
4638 .iter()
4639 .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
4640 .cloned()
4641 .collect();
4642 Ok(VmValue::Set(Box::new(result)))
4643 }
4644 _ => Err(runtime_err("set_intersection() expects two sets")),
4645 }
4646 }
4647 BuiltinId::SetDifference => {
4648 if args.len() < 2 {
4649 return Err(runtime_err("set_difference() expects 2 arguments"));
4650 }
4651 match (&args[0], &args[1]) {
4652 (VmValue::Set(a), VmValue::Set(b)) => {
4653 let result: Vec<VmValue> = a
4654 .iter()
4655 .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
4656 .cloned()
4657 .collect();
4658 Ok(VmValue::Set(Box::new(result)))
4659 }
4660 _ => Err(runtime_err("set_difference() expects two sets")),
4661 }
4662 }
4663
4664 #[cfg(feature = "native")]
4666 BuiltinId::FillNull => {
4667 if args.len() < 2 {
4668 return Err(runtime_err(
4669 "fill_null() expects (table, column, [strategy], [value])",
4670 ));
4671 }
4672 let df = match &args[0] {
4673 VmValue::Table(t) => t.df.clone(),
4674 _ => return Err(runtime_err("fill_null() first arg must be a table")),
4675 };
4676 let column = match &args[1] {
4677 VmValue::String(s) => s.to_string(),
4678 _ => return Err(runtime_err("fill_null() column must be a string")),
4679 };
4680 let strategy = if args.len() > 2 {
4681 match &args[2] {
4682 VmValue::String(s) => s.to_string(),
4683 _ => "value".to_string(),
4684 }
4685 } else {
4686 "value".to_string()
4687 };
4688 let fill_value = if args.len() > 3 {
4689 match &args[3] {
4690 VmValue::Int(n) => Some(*n as f64),
4691 VmValue::Float(f) => Some(*f),
4692 _ => None,
4693 }
4694 } else if args.len() > 2 && strategy == "value" {
4695 match &args[2] {
4696 VmValue::Int(n) => {
4697 return Ok(VmValue::Table(VmTable {
4698 df: self
4699 .engine()
4700 .fill_null(df, &column, "value", Some(*n as f64))
4701 .map_err(runtime_err)?,
4702 }));
4703 }
4704 VmValue::Float(f) => {
4705 return Ok(VmValue::Table(VmTable {
4706 df: self
4707 .engine()
4708 .fill_null(df, &column, "value", Some(*f))
4709 .map_err(runtime_err)?,
4710 }));
4711 }
4712 _ => None,
4713 }
4714 } else {
4715 None
4716 };
4717 let result = self
4718 .engine()
4719 .fill_null(df, &column, &strategy, fill_value)
4720 .map_err(runtime_err)?;
4721 Ok(VmValue::Table(VmTable { df: result }))
4722 }
4723 #[cfg(feature = "native")]
4724 BuiltinId::DropNull => {
4725 if args.len() < 2 {
4726 return Err(runtime_err("drop_null() expects (table, column)"));
4727 }
4728 let df = match &args[0] {
4729 VmValue::Table(t) => t.df.clone(),
4730 _ => return Err(runtime_err("drop_null() first arg must be a table")),
4731 };
4732 let column = match &args[1] {
4733 VmValue::String(s) => s.to_string(),
4734 _ => return Err(runtime_err("drop_null() column must be a string")),
4735 };
4736 let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
4737 Ok(VmValue::Table(VmTable { df: result }))
4738 }
4739 #[cfg(feature = "native")]
4740 BuiltinId::Dedup => {
4741 if args.is_empty() {
4742 return Err(runtime_err("dedup() expects (table, [columns...])"));
4743 }
4744 let df = match &args[0] {
4745 VmValue::Table(t) => t.df.clone(),
4746 _ => return Err(runtime_err("dedup() first arg must be a table")),
4747 };
4748 let columns: Vec<String> = args[1..]
4749 .iter()
4750 .filter_map(|a| {
4751 if let VmValue::String(s) = a {
4752 Some(s.to_string())
4753 } else {
4754 None
4755 }
4756 })
4757 .collect();
4758 let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
4759 Ok(VmValue::Table(VmTable { df: result }))
4760 }
4761 #[cfg(feature = "native")]
4762 BuiltinId::Clamp => {
4763 if args.len() < 4 {
4764 return Err(runtime_err("clamp() expects (table, column, min, max)"));
4765 }
4766 let df = match &args[0] {
4767 VmValue::Table(t) => t.df.clone(),
4768 _ => return Err(runtime_err("clamp() first arg must be a table")),
4769 };
4770 let column = match &args[1] {
4771 VmValue::String(s) => s.to_string(),
4772 _ => return Err(runtime_err("clamp() column must be a string")),
4773 };
4774 let min_val = match &args[2] {
4775 VmValue::Int(n) => *n as f64,
4776 VmValue::Float(f) => *f,
4777 _ => return Err(runtime_err("clamp() min must be a number")),
4778 };
4779 let max_val = match &args[3] {
4780 VmValue::Int(n) => *n as f64,
4781 VmValue::Float(f) => *f,
4782 _ => return Err(runtime_err("clamp() max must be a number")),
4783 };
4784 let result = self
4785 .engine()
4786 .clamp(df, &column, min_val, max_val)
4787 .map_err(runtime_err)?;
4788 Ok(VmValue::Table(VmTable { df: result }))
4789 }
4790 #[cfg(feature = "native")]
4791 BuiltinId::DataProfile => {
4792 if args.is_empty() {
4793 return Err(runtime_err("data_profile() expects (table)"));
4794 }
4795 let df = match &args[0] {
4796 VmValue::Table(t) => t.df.clone(),
4797 _ => return Err(runtime_err("data_profile() arg must be a table")),
4798 };
4799 let result = self.engine().data_profile(df).map_err(runtime_err)?;
4800 Ok(VmValue::Table(VmTable { df: result }))
4801 }
4802 #[cfg(feature = "native")]
4803 BuiltinId::RowCount => {
4804 if args.is_empty() {
4805 return Err(runtime_err("row_count() expects (table)"));
4806 }
4807 let df = match &args[0] {
4808 VmValue::Table(t) => t.df.clone(),
4809 _ => return Err(runtime_err("row_count() arg must be a table")),
4810 };
4811 let count = self.engine().row_count(df).map_err(runtime_err)?;
4812 Ok(VmValue::Int(count))
4813 }
4814 #[cfg(feature = "native")]
4815 BuiltinId::NullRate => {
4816 if args.len() < 2 {
4817 return Err(runtime_err("null_rate() expects (table, column)"));
4818 }
4819 let df = match &args[0] {
4820 VmValue::Table(t) => t.df.clone(),
4821 _ => return Err(runtime_err("null_rate() first arg must be a table")),
4822 };
4823 let column = match &args[1] {
4824 VmValue::String(s) => s.to_string(),
4825 _ => return Err(runtime_err("null_rate() column must be a string")),
4826 };
4827 let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
4828 Ok(VmValue::Float(rate))
4829 }
4830 #[cfg(feature = "native")]
4831 BuiltinId::IsUnique => {
4832 if args.len() < 2 {
4833 return Err(runtime_err("is_unique() expects (table, column)"));
4834 }
4835 let df = match &args[0] {
4836 VmValue::Table(t) => t.df.clone(),
4837 _ => return Err(runtime_err("is_unique() first arg must be a table")),
4838 };
4839 let column = match &args[1] {
4840 VmValue::String(s) => s.to_string(),
4841 _ => return Err(runtime_err("is_unique() column must be a string")),
4842 };
4843 let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
4844 Ok(VmValue::Bool(unique))
4845 }
4846 #[cfg(not(feature = "native"))]
4847 BuiltinId::FillNull
4848 | BuiltinId::DropNull
4849 | BuiltinId::Dedup
4850 | BuiltinId::Clamp
4851 | BuiltinId::DataProfile
4852 | BuiltinId::RowCount
4853 | BuiltinId::NullRate
4854 | BuiltinId::IsUnique => Err(runtime_err("Data operations not available in WASM")),
4855 #[cfg(feature = "native")]
4856 BuiltinId::IsEmail => {
4857 if args.is_empty() {
4858 return Err(runtime_err("is_email() expects 1 argument"));
4859 }
4860 let s = match &args[0] {
4861 VmValue::String(s) => s.to_string(),
4862 _ => return Err(runtime_err("is_email() arg must be a string")),
4863 };
4864 Ok(VmValue::Bool(tl_data::validate::is_email(&s)))
4865 }
4866 #[cfg(feature = "native")]
4867 BuiltinId::IsUrl => {
4868 if args.is_empty() {
4869 return Err(runtime_err("is_url() expects 1 argument"));
4870 }
4871 let s = match &args[0] {
4872 VmValue::String(s) => s.to_string(),
4873 _ => return Err(runtime_err("is_url() arg must be a string")),
4874 };
4875 Ok(VmValue::Bool(tl_data::validate::is_url(&s)))
4876 }
4877 #[cfg(feature = "native")]
4878 BuiltinId::IsPhone => {
4879 if args.is_empty() {
4880 return Err(runtime_err("is_phone() expects 1 argument"));
4881 }
4882 let s = match &args[0] {
4883 VmValue::String(s) => s.to_string(),
4884 _ => return Err(runtime_err("is_phone() arg must be a string")),
4885 };
4886 Ok(VmValue::Bool(tl_data::validate::is_phone(&s)))
4887 }
4888 #[cfg(feature = "native")]
4889 BuiltinId::IsBetween => {
4890 if args.len() < 3 {
4891 return Err(runtime_err("is_between() expects (value, low, high)"));
4892 }
4893 let val = match &args[0] {
4894 VmValue::Int(n) => *n as f64,
4895 VmValue::Float(f) => *f,
4896 _ => return Err(runtime_err("is_between() value must be a number")),
4897 };
4898 let low = match &args[1] {
4899 VmValue::Int(n) => *n as f64,
4900 VmValue::Float(f) => *f,
4901 _ => return Err(runtime_err("is_between() low must be a number")),
4902 };
4903 let high = match &args[2] {
4904 VmValue::Int(n) => *n as f64,
4905 VmValue::Float(f) => *f,
4906 _ => return Err(runtime_err("is_between() high must be a number")),
4907 };
4908 Ok(VmValue::Bool(tl_data::validate::is_between(val, low, high)))
4909 }
4910 #[cfg(feature = "native")]
4911 BuiltinId::Levenshtein => {
4912 if args.len() < 2 {
4913 return Err(runtime_err("levenshtein() expects (str_a, str_b)"));
4914 }
4915 let a = match &args[0] {
4916 VmValue::String(s) => s.to_string(),
4917 _ => return Err(runtime_err("levenshtein() args must be strings")),
4918 };
4919 let b = match &args[1] {
4920 VmValue::String(s) => s.to_string(),
4921 _ => return Err(runtime_err("levenshtein() args must be strings")),
4922 };
4923 Ok(VmValue::Int(tl_data::validate::levenshtein(&a, &b) as i64))
4924 }
4925 #[cfg(feature = "native")]
4926 BuiltinId::Soundex => {
4927 if args.is_empty() {
4928 return Err(runtime_err("soundex() expects 1 argument"));
4929 }
4930 let s = match &args[0] {
4931 VmValue::String(s) => s.to_string(),
4932 _ => return Err(runtime_err("soundex() arg must be a string")),
4933 };
4934 Ok(VmValue::String(Arc::from(
4935 tl_data::validate::soundex(&s).as_str(),
4936 )))
4937 }
4938 #[cfg(not(feature = "native"))]
4939 BuiltinId::IsEmail
4940 | BuiltinId::IsUrl
4941 | BuiltinId::IsPhone
4942 | BuiltinId::IsBetween
4943 | BuiltinId::Levenshtein
4944 | BuiltinId::Soundex => Err(runtime_err("Data validation not available in WASM")),
4945 #[cfg(feature = "native")]
4946 BuiltinId::ReadMysql => {
4947 #[cfg(feature = "mysql")]
4948 {
4949 if args.len() < 2 {
4950 return Err(runtime_err("read_mysql() expects (conn_str, query)"));
4951 }
4952 let conn_str = match &args[0] {
4953 VmValue::String(s) => s.to_string(),
4954 _ => return Err(runtime_err("read_mysql() conn_str must be a string")),
4955 };
4956 let query = match &args[1] {
4957 VmValue::String(s) => s.to_string(),
4958 _ => return Err(runtime_err("read_mysql() query must be a string")),
4959 };
4960 let df = self
4961 .engine()
4962 .read_mysql(&conn_str, &query)
4963 .map_err(runtime_err)?;
4964 Ok(VmValue::Table(VmTable { df }))
4965 }
4966 #[cfg(not(feature = "mysql"))]
4967 Err(runtime_err("read_mysql() requires the 'mysql' feature"))
4968 }
4969 #[cfg(feature = "native")]
4970 BuiltinId::ReadSqlite => {
4971 #[cfg(feature = "sqlite")]
4972 {
4973 if args.len() < 2 {
4974 return Err(runtime_err("read_sqlite() expects (db_path, query)"));
4975 }
4976 let db_path = match &args[0] {
4977 VmValue::String(s) => s.to_string(),
4978 _ => return Err(runtime_err("read_sqlite() db_path must be a string")),
4979 };
4980 let query = match &args[1] {
4981 VmValue::String(s) => s.to_string(),
4982 _ => return Err(runtime_err("read_sqlite() query must be a string")),
4983 };
4984 let df = self
4985 .engine()
4986 .read_sqlite(&db_path, &query)
4987 .map_err(runtime_err)?;
4988 Ok(VmValue::Table(VmTable { df }))
4989 }
4990 #[cfg(not(feature = "sqlite"))]
4991 Err(runtime_err("read_sqlite() requires the 'sqlite' feature"))
4992 }
4993 #[cfg(feature = "native")]
4994 BuiltinId::WriteSqlite => {
4995 #[cfg(feature = "sqlite")]
4996 {
4997 if args.len() < 3 {
4998 return Err(runtime_err(
4999 "write_sqlite() expects (table, db_path, table_name)",
5000 ));
5001 }
5002 let df = match &args[0] {
5003 VmValue::Table(t) => t.df.clone(),
5004 _ => return Err(runtime_err("write_sqlite() first arg must be a table")),
5005 };
5006 let db_path = match &args[1] {
5007 VmValue::String(s) => s.to_string(),
5008 _ => return Err(runtime_err("write_sqlite() db_path must be a string")),
5009 };
5010 let table_name = match &args[2] {
5011 VmValue::String(s) => s.to_string(),
5012 _ => return Err(runtime_err("write_sqlite() table_name must be a string")),
5013 };
5014 self.engine()
5015 .write_sqlite(df, &db_path, &table_name)
5016 .map_err(runtime_err)?;
5017 Ok(VmValue::None)
5018 }
5019 #[cfg(not(feature = "sqlite"))]
5020 Err(runtime_err("write_sqlite() requires the 'sqlite' feature"))
5021 }
5022 #[cfg(feature = "native")]
5023 BuiltinId::ReadDuckDb => {
5024 #[cfg(feature = "duckdb")]
5025 {
5026 if args.len() < 2 {
5027 return Err(runtime_err("duckdb() expects (db_path, query)"));
5028 }
5029 let db_path = match &args[0] {
5030 VmValue::String(s) => s.to_string(),
5031 _ => return Err(runtime_err("duckdb() db_path must be a string")),
5032 };
5033 let query = match &args[1] {
5034 VmValue::String(s) => s.to_string(),
5035 _ => return Err(runtime_err("duckdb() query must be a string")),
5036 };
5037 let df = self
5038 .engine()
5039 .read_duckdb(&db_path, &query)
5040 .map_err(runtime_err)?;
5041 Ok(VmValue::Table(VmTable { df }))
5042 }
5043 #[cfg(not(feature = "duckdb"))]
5044 Err(runtime_err("duckdb() requires the 'duckdb' feature"))
5045 }
5046 #[cfg(feature = "native")]
5047 BuiltinId::WriteDuckDb => {
5048 #[cfg(feature = "duckdb")]
5049 {
5050 if args.len() < 3 {
5051 return Err(runtime_err(
5052 "write_duckdb() expects (table, db_path, table_name)",
5053 ));
5054 }
5055 let df = match &args[0] {
5056 VmValue::Table(t) => t.df.clone(),
5057 _ => return Err(runtime_err("write_duckdb() first arg must be a table")),
5058 };
5059 let db_path = match &args[1] {
5060 VmValue::String(s) => s.to_string(),
5061 _ => return Err(runtime_err("write_duckdb() db_path must be a string")),
5062 };
5063 let table_name = match &args[2] {
5064 VmValue::String(s) => s.to_string(),
5065 _ => return Err(runtime_err("write_duckdb() table_name must be a string")),
5066 };
5067 self.engine()
5068 .write_duckdb(df, &db_path, &table_name)
5069 .map_err(runtime_err)?;
5070 Ok(VmValue::None)
5071 }
5072 #[cfg(not(feature = "duckdb"))]
5073 Err(runtime_err("write_duckdb() requires the 'duckdb' feature"))
5074 }
5075 #[cfg(feature = "native")]
5076 BuiltinId::ReadRedshift => {
5077 if args.len() < 2 {
5078 return Err(runtime_err("redshift() expects (conn_str, query)"));
5079 }
5080 let conn_str = match &args[0] {
5081 VmValue::String(s) => {
5082 let s_str = s.to_string();
5083 resolve_tl_config_connection(&s_str)
5084 }
5085 _ => return Err(runtime_err("redshift() conn_str must be a string")),
5086 };
5087 let query = match &args[1] {
5088 VmValue::String(s) => s.to_string(),
5089 _ => return Err(runtime_err("redshift() query must be a string")),
5090 };
5091 let df = self
5092 .engine()
5093 .read_redshift(&conn_str, &query)
5094 .map_err(runtime_err)?;
5095 Ok(VmValue::Table(VmTable { df }))
5096 }
5097 #[cfg(feature = "native")]
5098 BuiltinId::ReadMssql => {
5099 #[cfg(feature = "mssql")]
5100 {
5101 if args.len() < 2 {
5102 return Err(runtime_err("mssql() expects (conn_str, query)"));
5103 }
5104 let conn_str = match &args[0] {
5105 VmValue::String(s) => {
5106 let s_str = s.to_string();
5107 resolve_tl_config_connection(&s_str)
5108 }
5109 _ => return Err(runtime_err("mssql() conn_str must be a string")),
5110 };
5111 let query = match &args[1] {
5112 VmValue::String(s) => s.to_string(),
5113 _ => return Err(runtime_err("mssql() query must be a string")),
5114 };
5115 let df = self
5116 .engine()
5117 .read_mssql(&conn_str, &query)
5118 .map_err(runtime_err)?;
5119 Ok(VmValue::Table(VmTable { df }))
5120 }
5121 #[cfg(not(feature = "mssql"))]
5122 Err(runtime_err("mssql() requires the 'mssql' feature"))
5123 }
5124 #[cfg(feature = "native")]
5125 BuiltinId::ReadSnowflake => {
5126 #[cfg(feature = "snowflake")]
5127 {
5128 if args.len() < 2 {
5129 return Err(runtime_err("snowflake() expects (config, query)"));
5130 }
5131 let config = match &args[0] {
5132 VmValue::String(s) => {
5133 let s_str = s.to_string();
5134 resolve_tl_config_connection(&s_str)
5135 }
5136 _ => return Err(runtime_err("snowflake() config must be a string")),
5137 };
5138 let query = match &args[1] {
5139 VmValue::String(s) => s.to_string(),
5140 _ => return Err(runtime_err("snowflake() query must be a string")),
5141 };
5142 let df = self
5143 .engine()
5144 .read_snowflake(&config, &query)
5145 .map_err(runtime_err)?;
5146 Ok(VmValue::Table(VmTable { df }))
5147 }
5148 #[cfg(not(feature = "snowflake"))]
5149 Err(runtime_err("snowflake() requires the 'snowflake' feature"))
5150 }
5151 #[cfg(feature = "native")]
5152 BuiltinId::ReadBigQuery => {
5153 #[cfg(feature = "bigquery")]
5154 {
5155 if args.len() < 2 {
5156 return Err(runtime_err("bigquery() expects (config, query)"));
5157 }
5158 let config = match &args[0] {
5159 VmValue::String(s) => {
5160 let s_str = s.to_string();
5161 resolve_tl_config_connection(&s_str)
5162 }
5163 _ => return Err(runtime_err("bigquery() config must be a string")),
5164 };
5165 let query = match &args[1] {
5166 VmValue::String(s) => s.to_string(),
5167 _ => return Err(runtime_err("bigquery() query must be a string")),
5168 };
5169 let df = self
5170 .engine()
5171 .read_bigquery(&config, &query)
5172 .map_err(runtime_err)?;
5173 Ok(VmValue::Table(VmTable { df }))
5174 }
5175 #[cfg(not(feature = "bigquery"))]
5176 Err(runtime_err("bigquery() requires the 'bigquery' feature"))
5177 }
5178 #[cfg(feature = "native")]
5179 BuiltinId::ReadDatabricks => {
5180 #[cfg(feature = "databricks")]
5181 {
5182 if args.len() < 2 {
5183 return Err(runtime_err("databricks() expects (config, query)"));
5184 }
5185 let config = match &args[0] {
5186 VmValue::String(s) => {
5187 let s_str = s.to_string();
5188 resolve_tl_config_connection(&s_str)
5189 }
5190 _ => return Err(runtime_err("databricks() config must be a string")),
5191 };
5192 let query = match &args[1] {
5193 VmValue::String(s) => s.to_string(),
5194 _ => return Err(runtime_err("databricks() query must be a string")),
5195 };
5196 let df = self
5197 .engine()
5198 .read_databricks(&config, &query)
5199 .map_err(runtime_err)?;
5200 Ok(VmValue::Table(VmTable { df }))
5201 }
5202 #[cfg(not(feature = "databricks"))]
5203 Err(runtime_err(
5204 "databricks() requires the 'databricks' feature",
5205 ))
5206 }
5207 #[cfg(feature = "native")]
5208 BuiltinId::ReadClickHouse => {
5209 #[cfg(feature = "clickhouse")]
5210 {
5211 if args.len() < 2 {
5212 return Err(runtime_err("clickhouse() expects (url, query)"));
5213 }
5214 let url = match &args[0] {
5215 VmValue::String(s) => {
5216 let s_str = s.to_string();
5217 resolve_tl_config_connection(&s_str)
5218 }
5219 _ => return Err(runtime_err("clickhouse() url must be a string")),
5220 };
5221 let query = match &args[1] {
5222 VmValue::String(s) => s.to_string(),
5223 _ => return Err(runtime_err("clickhouse() query must be a string")),
5224 };
5225 let df = self
5226 .engine()
5227 .read_clickhouse(&url, &query)
5228 .map_err(runtime_err)?;
5229 Ok(VmValue::Table(VmTable { df }))
5230 }
5231 #[cfg(not(feature = "clickhouse"))]
5232 Err(runtime_err(
5233 "clickhouse() requires the 'clickhouse' feature",
5234 ))
5235 }
5236 #[cfg(feature = "native")]
5237 BuiltinId::ReadMongo => {
5238 #[cfg(feature = "mongodb")]
5239 {
5240 if args.len() < 4 {
5241 return Err(runtime_err(
5242 "mongo() expects (conn_str, database, collection, filter_json)",
5243 ));
5244 }
5245 let conn_str = match &args[0] {
5246 VmValue::String(s) => {
5247 let s_str = s.to_string();
5248 resolve_tl_config_connection(&s_str)
5249 }
5250 _ => return Err(runtime_err("mongo() conn_str must be a string")),
5251 };
5252 let database = match &args[1] {
5253 VmValue::String(s) => s.to_string(),
5254 _ => return Err(runtime_err("mongo() database must be a string")),
5255 };
5256 let collection = match &args[2] {
5257 VmValue::String(s) => s.to_string(),
5258 _ => return Err(runtime_err("mongo() collection must be a string")),
5259 };
5260 let filter_json = match &args[3] {
5261 VmValue::String(s) => s.to_string(),
5262 _ => return Err(runtime_err("mongo() filter must be a string")),
5263 };
5264 let df = self
5265 .engine()
5266 .read_mongo(&conn_str, &database, &collection, &filter_json)
5267 .map_err(runtime_err)?;
5268 Ok(VmValue::Table(VmTable { df }))
5269 }
5270 #[cfg(not(feature = "mongodb"))]
5271 Err(runtime_err("mongo() requires the 'mongodb' feature"))
5272 }
5273 #[cfg(feature = "native")]
5274 BuiltinId::SftpDownload => {
5275 #[cfg(feature = "sftp")]
5276 {
5277 if args.len() < 3 {
5278 return Err(runtime_err(
5279 "sftp_download() expects (config, remote_path, local_path)",
5280 ));
5281 }
5282 let config = match &args[0] {
5283 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5284 _ => return Err(runtime_err("sftp_download() config must be a string")),
5285 };
5286 let remote = match &args[1] {
5287 VmValue::String(s) => s.to_string(),
5288 _ => {
5289 return Err(runtime_err(
5290 "sftp_download() remote_path must be a string",
5291 ));
5292 }
5293 };
5294 let local = match &args[2] {
5295 VmValue::String(s) => s.to_string(),
5296 _ => {
5297 return Err(runtime_err("sftp_download() local_path must be a string"));
5298 }
5299 };
5300 let result = self
5301 .engine()
5302 .sftp_download(&config, &remote, &local)
5303 .map_err(runtime_err)?;
5304 Ok(VmValue::String(Arc::from(result.as_str())))
5305 }
5306 #[cfg(not(feature = "sftp"))]
5307 Err(runtime_err("sftp_download() requires the 'sftp' feature"))
5308 }
5309 #[cfg(feature = "native")]
5310 BuiltinId::SftpUpload => {
5311 #[cfg(feature = "sftp")]
5312 {
5313 if args.len() < 3 {
5314 return Err(runtime_err(
5315 "sftp_upload() expects (config, local_path, remote_path)",
5316 ));
5317 }
5318 let config = match &args[0] {
5319 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5320 _ => return Err(runtime_err("sftp_upload() config must be a string")),
5321 };
5322 let local = match &args[1] {
5323 VmValue::String(s) => s.to_string(),
5324 _ => return Err(runtime_err("sftp_upload() local_path must be a string")),
5325 };
5326 let remote = match &args[2] {
5327 VmValue::String(s) => s.to_string(),
5328 _ => return Err(runtime_err("sftp_upload() remote_path must be a string")),
5329 };
5330 let result = self
5331 .engine()
5332 .sftp_upload(&config, &local, &remote)
5333 .map_err(runtime_err)?;
5334 Ok(VmValue::String(Arc::from(result.as_str())))
5335 }
5336 #[cfg(not(feature = "sftp"))]
5337 Err(runtime_err("sftp_upload() requires the 'sftp' feature"))
5338 }
5339 #[cfg(feature = "native")]
5340 BuiltinId::SftpList => {
5341 #[cfg(feature = "sftp")]
5342 {
5343 if args.len() < 2 {
5344 return Err(runtime_err("sftp_list() expects (config, remote_path)"));
5345 }
5346 let config = match &args[0] {
5347 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5348 _ => return Err(runtime_err("sftp_list() config must be a string")),
5349 };
5350 let remote = match &args[1] {
5351 VmValue::String(s) => s.to_string(),
5352 _ => return Err(runtime_err("sftp_list() remote_path must be a string")),
5353 };
5354 let df = self
5355 .engine()
5356 .sftp_list(&config, &remote)
5357 .map_err(runtime_err)?;
5358 Ok(VmValue::Table(VmTable { df }))
5359 }
5360 #[cfg(not(feature = "sftp"))]
5361 Err(runtime_err("sftp_list() requires the 'sftp' feature"))
5362 }
5363 #[cfg(feature = "native")]
5364 BuiltinId::SftpReadCsv => {
5365 #[cfg(feature = "sftp")]
5366 {
5367 if args.len() < 2 {
5368 return Err(runtime_err("sftp_read_csv() expects (config, remote_path)"));
5369 }
5370 let config = match &args[0] {
5371 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5372 _ => return Err(runtime_err("sftp_read_csv() config must be a string")),
5373 };
5374 let remote = match &args[1] {
5375 VmValue::String(s) => s.to_string(),
5376 _ => {
5377 return Err(runtime_err(
5378 "sftp_read_csv() remote_path must be a string",
5379 ));
5380 }
5381 };
5382 let df = self
5383 .engine()
5384 .sftp_read_csv(&config, &remote)
5385 .map_err(runtime_err)?;
5386 Ok(VmValue::Table(VmTable { df }))
5387 }
5388 #[cfg(not(feature = "sftp"))]
5389 Err(runtime_err("sftp_read_csv() requires the 'sftp' feature"))
5390 }
5391 #[cfg(feature = "native")]
5392 BuiltinId::SftpReadParquet => {
5393 #[cfg(feature = "sftp")]
5394 {
5395 if args.len() < 2 {
5396 return Err(runtime_err(
5397 "sftp_read_parquet() expects (config, remote_path)",
5398 ));
5399 }
5400 let config = match &args[0] {
5401 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5402 _ => {
5403 return Err(runtime_err("sftp_read_parquet() config must be a string"));
5404 }
5405 };
5406 let remote = match &args[1] {
5407 VmValue::String(s) => s.to_string(),
5408 _ => {
5409 return Err(runtime_err(
5410 "sftp_read_parquet() remote_path must be a string",
5411 ));
5412 }
5413 };
5414 let df = self
5415 .engine()
5416 .sftp_read_parquet(&config, &remote)
5417 .map_err(runtime_err)?;
5418 Ok(VmValue::Table(VmTable { df }))
5419 }
5420 #[cfg(not(feature = "sftp"))]
5421 Err(runtime_err(
5422 "sftp_read_parquet() requires the 'sftp' feature",
5423 ))
5424 }
5425 #[cfg(feature = "native")]
5426 BuiltinId::RedisConnect => {
5427 #[cfg(feature = "redis")]
5428 {
5429 if args.is_empty() {
5430 return Err(runtime_err("redis_connect() expects (url)"));
5431 }
5432 let url = match &args[0] {
5433 VmValue::String(s) => s.to_string(),
5434 _ => return Err(runtime_err("redis_connect() url must be a string")),
5435 };
5436 let result = tl_data::redis_conn::redis_connect(&url).map_err(runtime_err)?;
5437 Ok(VmValue::String(Arc::from(result.as_str())))
5438 }
5439 #[cfg(not(feature = "redis"))]
5440 Err(runtime_err("redis_connect() requires the 'redis' feature"))
5441 }
5442 #[cfg(feature = "native")]
5443 BuiltinId::RedisGet => {
5444 #[cfg(feature = "redis")]
5445 {
5446 if args.len() < 2 {
5447 return Err(runtime_err("redis_get() expects (url, key)"));
5448 }
5449 let url = match &args[0] {
5450 VmValue::String(s) => s.to_string(),
5451 _ => return Err(runtime_err("redis_get() url must be a string")),
5452 };
5453 let key = match &args[1] {
5454 VmValue::String(s) => s.to_string(),
5455 _ => return Err(runtime_err("redis_get() key must be a string")),
5456 };
5457 match tl_data::redis_conn::redis_get(&url, &key).map_err(runtime_err)? {
5458 Some(v) => Ok(VmValue::String(Arc::from(v.as_str()))),
5459 None => Ok(VmValue::None),
5460 }
5461 }
5462 #[cfg(not(feature = "redis"))]
5463 Err(runtime_err("redis_get() requires the 'redis' feature"))
5464 }
5465 #[cfg(feature = "native")]
5466 BuiltinId::RedisSet => {
5467 #[cfg(feature = "redis")]
5468 {
5469 if args.len() < 3 {
5470 return Err(runtime_err("redis_set() expects (url, key, value)"));
5471 }
5472 let url = match &args[0] {
5473 VmValue::String(s) => s.to_string(),
5474 _ => return Err(runtime_err("redis_set() url must be a string")),
5475 };
5476 let key = match &args[1] {
5477 VmValue::String(s) => s.to_string(),
5478 _ => return Err(runtime_err("redis_set() key must be a string")),
5479 };
5480 let value = match &args[2] {
5481 VmValue::String(s) => s.to_string(),
5482 _ => format!("{}", &args[2]),
5483 };
5484 tl_data::redis_conn::redis_set(&url, &key, &value).map_err(runtime_err)?;
5485 Ok(VmValue::None)
5486 }
5487 #[cfg(not(feature = "redis"))]
5488 Err(runtime_err("redis_set() requires the 'redis' feature"))
5489 }
5490 #[cfg(feature = "native")]
5491 BuiltinId::RedisDel => {
5492 #[cfg(feature = "redis")]
5493 {
5494 if args.len() < 2 {
5495 return Err(runtime_err("redis_del() expects (url, key)"));
5496 }
5497 let url = match &args[0] {
5498 VmValue::String(s) => s.to_string(),
5499 _ => return Err(runtime_err("redis_del() url must be a string")),
5500 };
5501 let key = match &args[1] {
5502 VmValue::String(s) => s.to_string(),
5503 _ => return Err(runtime_err("redis_del() key must be a string")),
5504 };
5505 let deleted =
5506 tl_data::redis_conn::redis_del(&url, &key).map_err(runtime_err)?;
5507 Ok(VmValue::Bool(deleted))
5508 }
5509 #[cfg(not(feature = "redis"))]
5510 Err(runtime_err("redis_del() requires the 'redis' feature"))
5511 }
5512 #[cfg(feature = "native")]
5513 BuiltinId::GraphqlQuery => {
5514 if args.len() < 2 {
5515 return Err(runtime_err(
5516 "graphql_query() expects (endpoint, query, [variables])",
5517 ));
5518 }
5519 let endpoint = match &args[0] {
5520 VmValue::String(s) => s.to_string(),
5521 _ => return Err(runtime_err("graphql_query() endpoint must be a string")),
5522 };
5523 let query = match &args[1] {
5524 VmValue::String(s) => s.to_string(),
5525 _ => return Err(runtime_err("graphql_query() query must be a string")),
5526 };
5527 let variables = if args.len() > 2 {
5528 vm_value_to_json(&args[2])
5529 } else {
5530 serde_json::Value::Null
5531 };
5532 let mut body = serde_json::Map::new();
5533 body.insert("query".to_string(), serde_json::Value::String(query));
5534 if !variables.is_null() {
5535 body.insert("variables".to_string(), variables);
5536 }
5537 let client = reqwest::blocking::Client::new();
5538 let resp = client
5539 .post(&endpoint)
5540 .header("Content-Type", "application/json")
5541 .json(&body)
5542 .send()
5543 .map_err(|e| runtime_err(format!("graphql_query() request error: {e}")))?;
5544 let text = resp
5545 .text()
5546 .map_err(|e| runtime_err(format!("graphql_query() response error: {e}")))?;
5547 let json: serde_json::Value = serde_json::from_str(&text)
5548 .map_err(|e| runtime_err(format!("graphql_query() JSON parse error: {e}")))?;
5549 Ok(vm_json_to_value(&json))
5550 }
5551 #[cfg(feature = "native")]
5552 BuiltinId::RegisterS3 => {
5553 #[cfg(feature = "s3")]
5554 {
5555 if args.len() < 2 {
5556 return Err(runtime_err(
5557 "register_s3() expects (bucket, region, [access_key], [secret_key], [endpoint])",
5558 ));
5559 }
5560 let bucket = match &args[0] {
5561 VmValue::String(s) => s.to_string(),
5562 _ => return Err(runtime_err("register_s3() bucket must be a string")),
5563 };
5564 let region = match &args[1] {
5565 VmValue::String(s) => s.to_string(),
5566 _ => return Err(runtime_err("register_s3() region must be a string")),
5567 };
5568 let access_key = args.get(2).and_then(|v| {
5569 if let VmValue::String(s) = v {
5570 Some(s.to_string())
5571 } else {
5572 None
5573 }
5574 });
5575 let secret_key = args.get(3).and_then(|v| {
5576 if let VmValue::String(s) = v {
5577 Some(s.to_string())
5578 } else {
5579 None
5580 }
5581 });
5582 let endpoint = args.get(4).and_then(|v| {
5583 if let VmValue::String(s) = v {
5584 Some(s.to_string())
5585 } else {
5586 None
5587 }
5588 });
5589 self.engine()
5590 .register_s3(
5591 &bucket,
5592 ®ion,
5593 access_key.as_deref(),
5594 secret_key.as_deref(),
5595 endpoint.as_deref(),
5596 )
5597 .map_err(runtime_err)?;
5598 Ok(VmValue::None)
5599 }
5600 #[cfg(not(feature = "s3"))]
5601 Err(runtime_err("register_s3() requires the 's3' feature"))
5602 }
5603 #[cfg(not(feature = "native"))]
5604 BuiltinId::ReadMysql
5605 | BuiltinId::ReadSqlite
5606 | BuiltinId::WriteSqlite
5607 | BuiltinId::ReadDuckDb
5608 | BuiltinId::WriteDuckDb
5609 | BuiltinId::ReadRedshift
5610 | BuiltinId::ReadMssql
5611 | BuiltinId::ReadSnowflake
5612 | BuiltinId::ReadBigQuery
5613 | BuiltinId::ReadDatabricks
5614 | BuiltinId::ReadClickHouse
5615 | BuiltinId::ReadMongo
5616 | BuiltinId::SftpDownload
5617 | BuiltinId::SftpUpload
5618 | BuiltinId::SftpList
5619 | BuiltinId::SftpReadCsv
5620 | BuiltinId::SftpReadParquet
5621 | BuiltinId::RedisConnect
5622 | BuiltinId::RedisGet
5623 | BuiltinId::RedisSet
5624 | BuiltinId::RedisDel
5625 | BuiltinId::GraphqlQuery
5626 | BuiltinId::RegisterS3 => Err(runtime_err("Connectors not available in WASM")),
5627 BuiltinId::PyImport => {
5629 self.check_permission("python")?;
5630 #[cfg(feature = "python")]
5631 {
5632 crate::python::py_import_impl(&args)
5633 }
5634 #[cfg(not(feature = "python"))]
5635 Err(runtime_err("py_import() requires the 'python' feature"))
5636 }
5637 BuiltinId::PyCall => {
5638 self.check_permission("python")?;
5639 #[cfg(feature = "python")]
5640 {
5641 crate::python::py_call_impl(&args)
5642 }
5643 #[cfg(not(feature = "python"))]
5644 Err(runtime_err("py_call() requires the 'python' feature"))
5645 }
5646 BuiltinId::PyEval => {
5647 self.check_permission("python")?;
5648 #[cfg(feature = "python")]
5649 {
5650 crate::python::py_eval_impl(&args)
5651 }
5652 #[cfg(not(feature = "python"))]
5653 Err(runtime_err("py_eval() requires the 'python' feature"))
5654 }
5655 BuiltinId::PyGetAttr => {
5656 self.check_permission("python")?;
5657 #[cfg(feature = "python")]
5658 {
5659 crate::python::py_getattr_impl(&args)
5660 }
5661 #[cfg(not(feature = "python"))]
5662 Err(runtime_err("py_getattr() requires the 'python' feature"))
5663 }
5664 BuiltinId::PySetAttr => {
5665 self.check_permission("python")?;
5666 #[cfg(feature = "python")]
5667 {
5668 crate::python::py_setattr_impl(&args)
5669 }
5670 #[cfg(not(feature = "python"))]
5671 Err(runtime_err("py_setattr() requires the 'python' feature"))
5672 }
5673 BuiltinId::PyToTl => {
5674 #[cfg(feature = "python")]
5675 {
5676 crate::python::py_to_tl_impl(&args)
5677 }
5678 #[cfg(not(feature = "python"))]
5679 Err(runtime_err("py_to_tl() requires the 'python' feature"))
5680 }
5681
5682 #[cfg(feature = "native")]
5684 BuiltinId::SchemaRegister => {
5685 let name = match args.first() {
5686 Some(VmValue::String(s)) => s.to_string(),
5687 _ => {
5688 return Err(runtime_err(
5689 "schema_register: first arg must be schema name string",
5690 ));
5691 }
5692 };
5693 let version = match args.get(1) {
5694 Some(VmValue::Int(v)) => *v,
5695 _ => {
5696 return Err(runtime_err(
5697 "schema_register: second arg must be version number",
5698 ));
5699 }
5700 };
5701 let fields = match args.get(2) {
5702 Some(VmValue::Map(pairs)) => {
5703 let mut arrow_fields = Vec::new();
5704 for (k, v) in pairs.iter() {
5705 let fname = k.to_string();
5706 let ftype = match v {
5707 VmValue::String(s) => s.to_string(),
5708 _ => "string".to_string(),
5709 };
5710 arrow_fields.push(tl_data::ArrowField::new(
5711 &fname,
5712 crate::schema::type_name_to_arrow_pub(&ftype),
5713 true,
5714 ));
5715 }
5716 arrow_fields
5717 }
5718 _ => return Err(runtime_err("schema_register: third arg must be fields map")),
5719 };
5720 let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(fields));
5721 self.schema_registry
5722 .register(
5723 &name,
5724 version,
5725 schema,
5726 crate::schema::SchemaMetadata::default(),
5727 )
5728 .map_err(|e| runtime_err(&e))?;
5729 Ok(VmValue::None)
5730 }
5731 #[cfg(feature = "native")]
5732 BuiltinId::SchemaGet => {
5733 let name = match args.first() {
5734 Some(VmValue::String(s)) => s.to_string(),
5735 _ => return Err(runtime_err("schema_get: need name")),
5736 };
5737 let version = match args.get(1) {
5738 Some(VmValue::Int(v)) => *v,
5739 _ => return Err(runtime_err("schema_get: need version")),
5740 };
5741 match self.schema_registry.get(&name, version) {
5742 Some(vs) => {
5743 let fields: Vec<VmValue> = vs
5744 .schema
5745 .fields()
5746 .iter()
5747 .map(|f| {
5748 VmValue::String(std::sync::Arc::from(format!(
5749 "{}: {}",
5750 f.name(),
5751 f.data_type()
5752 )))
5753 })
5754 .collect();
5755 Ok(VmValue::List(Box::new(fields)))
5756 }
5757 None => Ok(VmValue::None),
5758 }
5759 }
5760 #[cfg(feature = "native")]
5761 BuiltinId::SchemaLatest => {
5762 let name = match args.first() {
5763 Some(VmValue::String(s)) => s.to_string(),
5764 _ => return Err(runtime_err("schema_latest: need name")),
5765 };
5766 match self.schema_registry.latest(&name) {
5767 Some(vs) => Ok(VmValue::Int(vs.version)),
5768 None => Ok(VmValue::None),
5769 }
5770 }
5771 #[cfg(feature = "native")]
5772 BuiltinId::SchemaHistory => {
5773 let name = match args.first() {
5774 Some(VmValue::String(s)) => s.to_string(),
5775 _ => return Err(runtime_err("schema_history: need name")),
5776 };
5777 let versions = self.schema_registry.versions(&name);
5778 Ok(VmValue::List(Box::new(
5779 versions.into_iter().map(VmValue::Int).collect(),
5780 )))
5781 }
5782 #[cfg(feature = "native")]
5783 BuiltinId::SchemaCheck => {
5784 let name = match args.first() {
5785 Some(VmValue::String(s)) => s.to_string(),
5786 _ => return Err(runtime_err("schema_check: need name")),
5787 };
5788 let v1 = match args.get(1) {
5789 Some(VmValue::Int(v)) => *v,
5790 _ => return Err(runtime_err("schema_check: need v1")),
5791 };
5792 let v2 = match args.get(2) {
5793 Some(VmValue::Int(v)) => *v,
5794 _ => return Err(runtime_err("schema_check: need v2")),
5795 };
5796 let mode_str = match args.get(3) {
5797 Some(VmValue::String(s)) => s.to_string(),
5798 _ => "backward".to_string(),
5799 };
5800 let mode = crate::schema::CompatibilityMode::from_str(&mode_str);
5801 let issues = self
5802 .schema_registry
5803 .check_compatibility(&name, v1, v2, mode);
5804 Ok(VmValue::List(Box::new(
5805 issues
5806 .into_iter()
5807 .map(|i| VmValue::String(std::sync::Arc::from(i.to_string())))
5808 .collect(),
5809 )))
5810 }
5811 #[cfg(feature = "native")]
5812 BuiltinId::SchemaDiff => {
5813 let name = match args.first() {
5814 Some(VmValue::String(s)) => s.to_string(),
5815 _ => return Err(runtime_err("schema_diff: need name")),
5816 };
5817 let v1 = match args.get(1) {
5818 Some(VmValue::Int(v)) => *v,
5819 _ => return Err(runtime_err("schema_diff: need v1")),
5820 };
5821 let v2 = match args.get(2) {
5822 Some(VmValue::Int(v)) => *v,
5823 _ => return Err(runtime_err("schema_diff: need v2")),
5824 };
5825 let diffs = self.schema_registry.diff(&name, v1, v2);
5826 Ok(VmValue::List(Box::new(
5827 diffs
5828 .into_iter()
5829 .map(|d| VmValue::String(std::sync::Arc::from(d.to_string())))
5830 .collect(),
5831 )))
5832 }
5833 #[cfg(feature = "native")]
5834 BuiltinId::SchemaApplyMigration => {
5835 let name = match args.first() {
5836 Some(VmValue::String(s)) => s.to_string(),
5837 _ => return Err(runtime_err("schema_apply_migration: need name")),
5838 };
5839 let from_v = match args.get(1) {
5840 Some(VmValue::Int(v)) => *v,
5841 _ => return Err(runtime_err("schema_apply_migration: need from_ver")),
5842 };
5843 let to_v = match args.get(2) {
5844 Some(VmValue::Int(v)) => *v,
5845 _ => return Err(runtime_err("schema_apply_migration: need to_ver")),
5846 };
5847 Ok(VmValue::String(std::sync::Arc::from(format!(
5848 "migration {}:{}->{} applied",
5849 name, from_v, to_v
5850 ))))
5851 }
5852 #[cfg(feature = "native")]
5853 BuiltinId::SchemaVersions => {
5854 let name = match args.first() {
5855 Some(VmValue::String(s)) => s.to_string(),
5856 _ => return Err(runtime_err("schema_versions: need name")),
5857 };
5858 let versions = self.schema_registry.versions(&name);
5859 Ok(VmValue::List(Box::new(
5860 versions.into_iter().map(VmValue::Int).collect(),
5861 )))
5862 }
5863 #[cfg(feature = "native")]
5864 BuiltinId::SchemaFields => {
5865 let name = match args.first() {
5866 Some(VmValue::String(s)) => s.to_string(),
5867 _ => return Err(runtime_err("schema_fields: need name")),
5868 };
5869 let version = match args.get(1) {
5870 Some(VmValue::Int(v)) => *v,
5871 _ => return Err(runtime_err("schema_fields: need version")),
5872 };
5873 let fields = self.schema_registry.fields(&name, version);
5874 Ok(VmValue::List(Box::new(
5875 fields
5876 .into_iter()
5877 .map(|(n, t)| {
5878 VmValue::String(std::sync::Arc::from(format!("{}: {}", n, t)))
5879 })
5880 .collect(),
5881 )))
5882 }
5883 #[cfg(not(feature = "native"))]
5884 BuiltinId::SchemaRegister
5885 | BuiltinId::SchemaGet
5886 | BuiltinId::SchemaLatest
5887 | BuiltinId::SchemaHistory
5888 | BuiltinId::SchemaCheck
5889 | BuiltinId::SchemaDiff
5890 | BuiltinId::SchemaApplyMigration
5891 | BuiltinId::SchemaVersions
5892 | BuiltinId::SchemaFields => {
5893 let _ = args;
5894 Err(runtime_err("Schema operations not available in WASM"))
5895 }
5896
5897 BuiltinId::Decimal => {
5899 use std::str::FromStr;
5900 let s = match args.first() {
5901 Some(VmValue::String(s)) => s.to_string(),
5902 Some(VmValue::Int(n)) => n.to_string(),
5903 Some(VmValue::Float(f)) => f.to_string(),
5904 _ => return Err(runtime_err("decimal(): expected string, int, or float")),
5905 };
5906 let d = rust_decimal::Decimal::from_str(&s)
5907 .map_err(|e| runtime_err(format!("decimal(): invalid: {e}")))?;
5908 Ok(VmValue::Decimal(d))
5909 }
5910
5911 BuiltinId::SecretGet => {
5913 let key = match args.first() {
5914 Some(VmValue::String(s)) => s.to_string(),
5915 _ => return Err(runtime_err("secret_get: need key")),
5916 };
5917 if let Some(val) = self.secret_vault.get(&key) {
5918 Ok(VmValue::Secret(Arc::from(val.as_str())))
5919 } else {
5920 let env_key = format!("TL_SECRET_{}", key.to_uppercase());
5922 match std::env::var(&env_key) {
5923 Ok(val) => Ok(VmValue::Secret(Arc::from(val.as_str()))),
5924 Err(_) => Ok(VmValue::None),
5925 }
5926 }
5927 }
5928 BuiltinId::SecretSet => {
5929 let key = match args.first() {
5930 Some(VmValue::String(s)) => s.to_string(),
5931 _ => return Err(runtime_err("secret_set: need key")),
5932 };
5933 let val = match args.get(1) {
5934 Some(VmValue::String(s)) => s.to_string(),
5935 Some(VmValue::Secret(s)) => s.to_string(),
5936 _ => return Err(runtime_err("secret_set: need value")),
5937 };
5938 self.secret_vault.insert(key, val);
5939 Ok(VmValue::None)
5940 }
5941 BuiltinId::SecretDelete => {
5942 let key = match args.first() {
5943 Some(VmValue::String(s)) => s.to_string(),
5944 _ => return Err(runtime_err("secret_delete: need key")),
5945 };
5946 self.secret_vault.remove(&key);
5947 Ok(VmValue::None)
5948 }
5949 BuiltinId::SecretList => {
5950 let keys: Vec<VmValue> = self
5951 .secret_vault
5952 .keys()
5953 .map(|k| VmValue::String(Arc::from(k.as_str())))
5954 .collect();
5955 Ok(VmValue::List(Box::new(keys)))
5956 }
5957 BuiltinId::CheckPermission => {
5958 let perm = match args.first() {
5959 Some(VmValue::String(s)) => s.to_string(),
5960 _ => return Err(runtime_err("check_permission: need permission name")),
5961 };
5962 let allowed = match self.security_policy {
5963 Some(ref policy) => policy.check(&perm),
5964 None => true,
5965 };
5966 Ok(VmValue::Bool(allowed))
5967 }
5968 BuiltinId::MaskEmail => {
5969 let email = match args.first() {
5970 Some(VmValue::String(s)) => s.to_string(),
5971 _ => return Err(runtime_err("mask_email: need string")),
5972 };
5973 let masked = if let Some(at_pos) = email.find('@') {
5974 let local = &email[..at_pos];
5975 let domain = &email[at_pos..];
5976 if local.len() > 1 {
5977 format!("{}***{}", &local[..1], domain)
5978 } else {
5979 format!("***{domain}")
5980 }
5981 } else {
5982 "***".to_string()
5983 };
5984 Ok(VmValue::String(Arc::from(masked.as_str())))
5985 }
5986 BuiltinId::MaskPhone => {
5987 let phone = match args.first() {
5988 Some(VmValue::String(s)) => s.to_string(),
5989 _ => return Err(runtime_err("mask_phone: need string")),
5990 };
5991 let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
5992 let masked = if digits.len() >= 4 {
5993 let last4 = &digits[digits.len() - 4..];
5994 format!("***-***-{last4}")
5995 } else {
5996 "***".to_string()
5997 };
5998 Ok(VmValue::String(Arc::from(masked.as_str())))
5999 }
6000 BuiltinId::MaskCreditCard => {
6001 let cc = match args.first() {
6002 Some(VmValue::String(s)) => s.to_string(),
6003 _ => return Err(runtime_err("mask_cc: need string")),
6004 };
6005 let digits: String = cc.chars().filter(|c| c.is_ascii_digit()).collect();
6006 let masked = if digits.len() >= 4 {
6007 let last4 = &digits[digits.len() - 4..];
6008 format!("****-****-****-{last4}")
6009 } else {
6010 "****-****-****-****".to_string()
6011 };
6012 Ok(VmValue::String(Arc::from(masked.as_str())))
6013 }
6014 BuiltinId::Redact => {
6015 let val = match args.first() {
6016 Some(v) => format!("{v}"),
6017 _ => return Err(runtime_err("redact: need value")),
6018 };
6019 let policy = match args.get(1) {
6020 Some(VmValue::String(s)) => s.to_string(),
6021 _ => "full".to_string(),
6022 };
6023 let result = match policy.as_str() {
6024 "full" => "***".to_string(),
6025 "partial" => {
6026 if val.len() > 2 {
6027 format!("{}***{}", &val[..1], &val[val.len() - 1..])
6028 } else {
6029 "***".to_string()
6030 }
6031 }
6032 "hash" => {
6033 use sha2::Digest;
6034 let hash = sha2::Sha256::digest(val.as_bytes());
6035 format!("{:x}", hash)
6036 }
6037 _ => "***".to_string(),
6038 };
6039 Ok(VmValue::String(Arc::from(result.as_str())))
6040 }
6041 BuiltinId::Hash => {
6042 let val = match args.first() {
6043 Some(VmValue::String(s)) => s.to_string(),
6044 _ => return Err(runtime_err("hash: need string")),
6045 };
6046 let algo = match args.get(1) {
6047 Some(VmValue::String(s)) => s.to_string(),
6048 _ => "sha256".to_string(),
6049 };
6050 let result = match algo.as_str() {
6051 "sha256" => {
6052 use sha2::Digest;
6053 format!("{:x}", sha2::Sha256::digest(val.as_bytes()))
6054 }
6055 "sha512" => {
6056 use sha2::Digest;
6057 format!("{:x}", sha2::Sha512::digest(val.as_bytes()))
6058 }
6059 "md5" => {
6060 use md5::Digest;
6061 format!("{:x}", md5::Md5::digest(val.as_bytes()))
6062 }
6063 _ => {
6064 return Err(runtime_err(format!(
6065 "hash: unknown algorithm '{algo}' (use sha256, sha512, or md5)"
6066 )));
6067 }
6068 };
6069 Ok(VmValue::String(Arc::from(result.as_str())))
6070 }
6071
6072 #[cfg(feature = "async-runtime")]
6074 BuiltinId::AsyncReadFile => {
6075 let rt = self.ensure_runtime();
6076 crate::async_runtime::async_read_file_impl(&rt, &args, &self.security_policy)
6077 }
6078 #[cfg(feature = "async-runtime")]
6079 BuiltinId::AsyncWriteFile => {
6080 let rt = self.ensure_runtime();
6081 crate::async_runtime::async_write_file_impl(&rt, &args, &self.security_policy)
6082 }
6083 #[cfg(feature = "async-runtime")]
6084 BuiltinId::AsyncHttpGet => {
6085 let rt = self.ensure_runtime();
6086 crate::async_runtime::async_http_get_impl(&rt, &args, &self.security_policy)
6087 }
6088 #[cfg(feature = "async-runtime")]
6089 BuiltinId::AsyncHttpPost => {
6090 let rt = self.ensure_runtime();
6091 crate::async_runtime::async_http_post_impl(&rt, &args, &self.security_policy)
6092 }
6093 #[cfg(feature = "async-runtime")]
6094 BuiltinId::AsyncSleep => {
6095 let rt = self.ensure_runtime();
6096 crate::async_runtime::async_sleep_impl(&rt, &args)
6097 }
6098 #[cfg(feature = "async-runtime")]
6099 BuiltinId::Select => crate::async_runtime::select_impl(&args),
6100 #[cfg(feature = "async-runtime")]
6101 BuiltinId::RaceAll => crate::async_runtime::race_all_impl(&args),
6102 #[cfg(feature = "async-runtime")]
6103 BuiltinId::AsyncMap => {
6104 let rt = self.ensure_runtime();
6105 let stack_snapshot = self.stack.clone();
6106 crate::async_runtime::async_map_impl(&rt, &args, &self.globals, &stack_snapshot)
6107 }
6108 #[cfg(feature = "async-runtime")]
6109 BuiltinId::AsyncFilter => {
6110 let rt = self.ensure_runtime();
6111 let stack_snapshot = self.stack.clone();
6112 crate::async_runtime::async_filter_impl(&rt, &args, &self.globals, &stack_snapshot)
6113 }
6114
6115 #[cfg(not(feature = "async-runtime"))]
6116 BuiltinId::AsyncReadFile
6117 | BuiltinId::AsyncWriteFile
6118 | BuiltinId::AsyncHttpGet
6119 | BuiltinId::AsyncHttpPost
6120 | BuiltinId::AsyncSleep
6121 | BuiltinId::Select
6122 | BuiltinId::AsyncMap
6123 | BuiltinId::AsyncFilter
6124 | BuiltinId::RaceAll => Err(runtime_err(format!(
6125 "{}: async builtins require the 'async-runtime' feature",
6126 builtin_id.name()
6127 ))),
6128
6129 BuiltinId::IsError => {
6131 if args.is_empty() {
6132 return Err(runtime_err("is_error() expects 1 argument"));
6133 }
6134 let is_err = matches!(&args[0], VmValue::EnumInstance(e) if
6135 &*e.type_name == "DataError" ||
6136 &*e.type_name == "NetworkError" ||
6137 &*e.type_name == "ConnectorError"
6138 );
6139 Ok(VmValue::Bool(is_err))
6140 }
6141 BuiltinId::ErrorType => {
6142 if args.is_empty() {
6143 return Err(runtime_err("error_type() expects 1 argument"));
6144 }
6145 match &args[0] {
6146 VmValue::EnumInstance(e) => Ok(VmValue::String(e.type_name.clone())),
6147 _ => Ok(VmValue::None),
6148 }
6149 }
6150
6151 #[cfg(feature = "gpu")]
6153 BuiltinId::GpuAvailable => Ok(VmValue::Bool(tl_gpu::GpuDevice::is_available())),
6154 #[cfg(not(feature = "gpu"))]
6155 BuiltinId::GpuAvailable => Ok(VmValue::Bool(false)),
6156
6157 #[cfg(feature = "gpu")]
6158 BuiltinId::ToGpu => {
6159 if args.is_empty() {
6160 return Err(runtime_err("to_gpu() expects 1 argument (tensor)"));
6161 }
6162 let gt = self.ensure_gpu_tensor(&args[0])?;
6163 Ok(VmValue::GpuTensor(gt))
6164 }
6165 #[cfg(not(feature = "gpu"))]
6166 BuiltinId::ToGpu => Err(runtime_err(
6167 "GPU operations not available. Build with --features gpu",
6168 )),
6169
6170 #[cfg(feature = "gpu")]
6171 BuiltinId::ToCpu => {
6172 if args.is_empty() {
6173 return Err(runtime_err("to_cpu() expects 1 argument (gpu_tensor)"));
6174 }
6175 match &args[0] {
6176 VmValue::GpuTensor(gt) => {
6177 let cpu = gt.to_cpu().map_err(runtime_err)?;
6178 Ok(VmValue::Tensor(Arc::new(cpu)))
6179 }
6180 _ => Err(runtime_err(format!(
6181 "to_cpu() expects a gpu_tensor, got {}",
6182 args[0].type_name()
6183 ))),
6184 }
6185 }
6186 #[cfg(not(feature = "gpu"))]
6187 BuiltinId::ToCpu => Err(runtime_err(
6188 "GPU operations not available. Build with --features gpu",
6189 )),
6190
6191 #[cfg(feature = "gpu")]
6192 BuiltinId::GpuMatmul => {
6193 if args.len() < 2 {
6194 return Err(runtime_err("gpu_matmul() expects 2 arguments"));
6195 }
6196 let a = self.ensure_gpu_tensor(&args[0])?;
6197 let b = self.ensure_gpu_tensor(&args[1])?;
6198 let ops = self.get_gpu_ops()?;
6199 let result = ops.matmul(&a, &b).map_err(runtime_err)?;
6200 Ok(VmValue::GpuTensor(Arc::new(result)))
6201 }
6202 #[cfg(not(feature = "gpu"))]
6203 BuiltinId::GpuMatmul => Err(runtime_err(
6204 "GPU operations not available. Build with --features gpu",
6205 )),
6206
6207 #[cfg(feature = "gpu")]
6208 BuiltinId::GpuBatchPredict => {
6209 if args.len() < 2 {
6210 return Err(runtime_err("gpu_batch_predict() expects 2-3 arguments"));
6211 }
6212 match (&args[0], &args[1]) {
6213 (VmValue::Model(model), VmValue::Tensor(input)) => {
6214 let batch_size = args.get(2).and_then(|v| match v {
6215 VmValue::Int(n) => Some(*n as usize),
6216 _ => None,
6217 });
6218 let result =
6219 tl_gpu::BatchInference::batch_predict(model, input, batch_size)
6220 .map_err(runtime_err)?;
6221 Ok(VmValue::Tensor(Arc::new(result)))
6222 }
6223 _ => Err(runtime_err(
6224 "gpu_batch_predict() expects (model, tensor, [batch_size])",
6225 )),
6226 }
6227 }
6228 #[cfg(not(feature = "gpu"))]
6229 BuiltinId::GpuBatchPredict => Err(runtime_err(
6230 "GPU operations not available. Build with --features gpu",
6231 )),
6232 #[cfg(feature = "native")]
6234 BuiltinId::Embed => {
6235 if args.is_empty() {
6236 return Err(runtime_err("embed() requires a text argument"));
6237 }
6238 let text = match &args[0] {
6239 VmValue::String(s) => s.to_string(),
6240 _ => return Err(runtime_err("embed() expects a string")),
6241 };
6242 let model = args
6243 .get(1)
6244 .and_then(|v| match v {
6245 VmValue::String(s) => Some(s.to_string()),
6246 _ => None,
6247 })
6248 .unwrap_or_else(|| "text-embedding-3-small".to_string());
6249 let api_key = args
6250 .get(2)
6251 .and_then(|v| match v {
6252 VmValue::String(s) => Some(s.to_string()),
6253 _ => None,
6254 })
6255 .or_else(|| std::env::var("TL_OPENAI_KEY").ok())
6256 .ok_or_else(|| {
6257 runtime_err(
6258 "embed() requires an API key. Set TL_OPENAI_KEY or pass as 3rd arg",
6259 )
6260 })?;
6261 let tensor = tl_ai::embed::embed_api(&text, "openai", &model, &api_key)
6262 .map_err(|e| runtime_err(format!("embed error: {e}")))?;
6263 Ok(VmValue::Tensor(Arc::new(tensor)))
6264 }
6265 #[cfg(not(feature = "native"))]
6266 BuiltinId::Embed => Err(runtime_err("embed() not available in WASM")),
6267 #[cfg(feature = "native")]
6268 BuiltinId::HttpRequest => {
6269 self.check_permission("network")?;
6270 if args.len() < 2 {
6271 return Err(runtime_err(
6272 "http_request(method, url, headers?, body?) expects at least 2 args",
6273 ));
6274 }
6275 let method = match &args[0] {
6276 VmValue::String(s) => s.to_string(),
6277 _ => return Err(runtime_err("http_request() method must be a string")),
6278 };
6279 let url = match &args[1] {
6280 VmValue::String(s) => s.to_string(),
6281 _ => return Err(runtime_err("http_request() url must be a string")),
6282 };
6283 let client = reqwest::blocking::Client::new();
6284 let mut builder = match method.to_uppercase().as_str() {
6285 "GET" => client.get(&url),
6286 "POST" => client.post(&url),
6287 "PUT" => client.put(&url),
6288 "DELETE" => client.delete(&url),
6289 "PATCH" => client.patch(&url),
6290 "HEAD" => client.head(&url),
6291 _ => return Err(runtime_err(format!("Unsupported HTTP method: {method}"))),
6292 };
6293 if let Some(VmValue::Map(headers)) = args.get(2) {
6295 for (key, val) in headers.iter() {
6296 if let VmValue::String(v) = val {
6297 builder = builder.header(key.as_ref(), v.as_ref());
6298 }
6299 }
6300 }
6301 if let Some(VmValue::String(body)) = args.get(3) {
6303 builder = builder.body(body.as_ref().to_string());
6304 }
6305 let resp = builder
6306 .send()
6307 .map_err(|e| runtime_err(format!("HTTP error: {e}")))?;
6308 let status = resp.status().as_u16() as i64;
6309 let body = resp
6310 .text()
6311 .map_err(|e| runtime_err(format!("HTTP response error: {e}")))?;
6312 Ok(VmValue::Map(Box::new(vec![
6313 (Arc::from("status"), VmValue::Int(status)),
6314 (Arc::from("body"), VmValue::String(Arc::from(body.as_str()))),
6315 ])))
6316 }
6317 #[cfg(not(feature = "native"))]
6318 BuiltinId::HttpRequest => Err(runtime_err("http_request() not available in WASM")),
6319 #[cfg(feature = "native")]
6320 BuiltinId::RunAgent => {
6321 self.check_permission("network")?;
6322 if args.len() < 2 {
6323 return Err(runtime_err(
6324 "run_agent(agent, message, [history]) expects at least 2 arguments",
6325 ));
6326 }
6327 let agent_def = match &args[0] {
6328 VmValue::AgentDef(def) => def.clone(),
6329 _ => return Err(runtime_err("run_agent() first arg must be an agent")),
6330 };
6331 let message = match &args[1] {
6332 VmValue::String(s) => s.to_string(),
6333 _ => return Err(runtime_err("run_agent() second arg must be a string")),
6334 };
6335 let history = if args.len() >= 3 {
6337 match &args[2] {
6338 VmValue::List(items) => {
6339 let mut hist = Vec::new();
6340 for item in items.iter() {
6341 if let VmValue::List(pair) = item
6342 && pair.len() >= 2
6343 {
6344 let role = match &pair[0] {
6345 VmValue::String(s) => s.to_string(),
6346 _ => continue,
6347 };
6348 let content = match &pair[1] {
6349 VmValue::String(s) => s.to_string(),
6350 _ => continue,
6351 };
6352 hist.push((role, content));
6353 }
6354 }
6355 Some(hist)
6356 }
6357 _ => None,
6358 }
6359 } else {
6360 None
6361 };
6362 self.exec_agent_loop(&agent_def, &message, history.as_deref())
6363 }
6364 #[cfg(not(feature = "native"))]
6365 BuiltinId::RunAgent => Err(runtime_err("run_agent() not available in WASM")),
6366
6367 #[cfg(feature = "native")]
6369 BuiltinId::StreamAgent => {
6370 self.check_permission("network")?;
6371 if args.len() < 3 {
6372 return Err(runtime_err(
6373 "stream_agent(agent, message, callback) expects 3 arguments",
6374 ));
6375 }
6376 let agent_def = match &args[0] {
6377 VmValue::AgentDef(def) => def.clone(),
6378 _ => return Err(runtime_err("stream_agent() first arg must be an agent")),
6379 };
6380 let message = match &args[1] {
6381 VmValue::String(s) => s.to_string(),
6382 _ => return Err(runtime_err("stream_agent() second arg must be a string")),
6383 };
6384 let callback = args[2].clone();
6385
6386 let model = &agent_def.model;
6387 let system = agent_def.system_prompt.as_deref();
6388 let base_url = agent_def.base_url.as_deref();
6389 let api_key = agent_def.api_key.as_deref();
6390
6391 let messages = vec![serde_json::json!({"role": "user", "content": &message})];
6392 let mut reader = tl_ai::stream_chat(model, system, &messages, base_url, api_key)
6393 .map_err(|e| runtime_err(format!("Stream error: {e}")))?;
6394
6395 let mut full_text = String::new();
6396 loop {
6397 match reader.next_chunk() {
6398 Ok(Some(chunk)) => {
6399 full_text.push_str(&chunk);
6400 let chunk_val = VmValue::String(Arc::from(&*chunk));
6401 let _ = self.call_value(callback.clone(), &[chunk_val]);
6402 }
6403 Ok(None) => break,
6404 Err(e) => return Err(runtime_err(format!("Stream error: {e}"))),
6405 }
6406 }
6407
6408 Ok(VmValue::String(Arc::from(&*full_text)))
6409 }
6410 #[cfg(not(feature = "native"))]
6411 BuiltinId::StreamAgent => Err(runtime_err("stream_agent() not available in WASM")),
6412
6413 #[cfg(feature = "native")]
6415 BuiltinId::Random => {
6416 let mut rng = rand::thread_rng();
6417 let val: f64 = rand::Rng::r#gen(&mut rng);
6418 Ok(VmValue::Float(val))
6419 }
6420 #[cfg(not(feature = "native"))]
6421 BuiltinId::Random => Err(runtime_err("random() not available in WASM")),
6422 #[cfg(feature = "native")]
6423 BuiltinId::RandomInt => {
6424 if args.len() < 2 {
6425 return Err(runtime_err("random_int() expects min and max"));
6426 }
6427 let a = match &args[0] {
6428 VmValue::Int(n) => *n,
6429 _ => return Err(runtime_err("random_int() expects integers")),
6430 };
6431 let b = match &args[1] {
6432 VmValue::Int(n) => *n,
6433 _ => return Err(runtime_err("random_int() expects integers")),
6434 };
6435 if a >= b {
6436 return Err(runtime_err("random_int() requires min < max"));
6437 }
6438 let mut rng = rand::thread_rng();
6439 let val: i64 = rand::Rng::gen_range(&mut rng, a..b);
6440 Ok(VmValue::Int(val))
6441 }
6442 #[cfg(not(feature = "native"))]
6443 BuiltinId::RandomInt => Err(runtime_err("random_int() not available in WASM")),
6444 #[cfg(feature = "native")]
6445 BuiltinId::Sample => {
6446 use rand::seq::SliceRandom;
6447 if args.is_empty() {
6448 return Err(runtime_err("sample() expects a list and count"));
6449 }
6450 let items = match &args[0] {
6451 VmValue::List(items) => items,
6452 _ => return Err(runtime_err("sample() expects a list")),
6453 };
6454 let k = match args.get(1) {
6455 Some(VmValue::Int(n)) => *n as usize,
6456 _ => 1,
6457 };
6458 if k > items.len() {
6459 return Err(runtime_err("sample() count exceeds list length"));
6460 }
6461 let mut rng = rand::thread_rng();
6462 let mut indices: Vec<usize> = (0..items.len()).collect();
6463 indices.partial_shuffle(&mut rng, k);
6464 let result: Vec<VmValue> = indices[..k].iter().map(|&i| items[i].clone()).collect();
6465 if k == 1 && args.get(1).is_none() {
6466 Ok(result.into_iter().next().unwrap_or(VmValue::None))
6467 } else {
6468 Ok(VmValue::List(Box::new(result)))
6469 }
6470 }
6471 #[cfg(not(feature = "native"))]
6472 BuiltinId::Sample => Err(runtime_err("sample() not available in WASM")),
6473
6474 BuiltinId::Exp => {
6476 let x = match args.first() {
6477 Some(VmValue::Float(f)) => *f,
6478 Some(VmValue::Int(n)) => *n as f64,
6479 _ => return Err(runtime_err("exp() expects a number")),
6480 };
6481 Ok(VmValue::Float(x.exp()))
6482 }
6483 BuiltinId::IsNan => {
6484 let result = match args.first() {
6485 Some(VmValue::Float(f)) => f.is_nan(),
6486 _ => false,
6487 };
6488 Ok(VmValue::Bool(result))
6489 }
6490 BuiltinId::IsInfinite => {
6491 let result = match args.first() {
6492 Some(VmValue::Float(f)) => f.is_infinite(),
6493 _ => false,
6494 };
6495 Ok(VmValue::Bool(result))
6496 }
6497 BuiltinId::Sign => match args.first() {
6498 Some(VmValue::Int(n)) => Ok(VmValue::Int(if *n > 0 {
6499 1
6500 } else if *n < 0 {
6501 -1
6502 } else {
6503 0
6504 })),
6505 Some(VmValue::Float(f)) => {
6506 if f.is_nan() {
6507 Ok(VmValue::Float(f64::NAN))
6508 } else if *f > 0.0 {
6509 Ok(VmValue::Int(1))
6510 } else if *f < 0.0 {
6511 Ok(VmValue::Int(-1))
6512 } else {
6513 Ok(VmValue::Int(0))
6514 }
6515 }
6516 _ => Err(runtime_err("sign() expects a number")),
6517 },
6518 #[cfg(feature = "native")]
6520 BuiltinId::AssertTableEq => {
6521 if args.len() < 2 {
6522 return Err(runtime_err("assert_table_eq() expects 2 table arguments"));
6523 }
6524 let t1 = match &args[0] {
6525 VmValue::Table(t) => t,
6526 _ => {
6527 return Err(runtime_err(
6528 "assert_table_eq() first argument must be a table",
6529 ));
6530 }
6531 };
6532 let t2 = match &args[1] {
6533 VmValue::Table(t) => t,
6534 _ => {
6535 return Err(runtime_err(
6536 "assert_table_eq() second argument must be a table",
6537 ));
6538 }
6539 };
6540 if t1.df.schema() != t2.df.schema() {
6542 return Err(runtime_err(format!(
6543 "assert_table_eq: schemas differ\n left: {:?}\n right: {:?}",
6544 t1.df.schema(),
6545 t2.df.schema()
6546 )));
6547 }
6548 let batches1 = self.engine().collect(t1.df.clone()).map_err(runtime_err)?;
6550 let batches2 = self.engine().collect(t2.df.clone()).map_err(runtime_err)?;
6551 let rows1: Vec<String> = batches1
6553 .iter()
6554 .flat_map(|b| {
6555 (0..b.num_rows()).map(move |r| {
6556 (0..b.num_columns())
6557 .map(|c| {
6558 let col = b.column(c);
6559 format!("{:?}", col.slice(r, 1))
6560 })
6561 .collect::<Vec<_>>()
6562 .join(",")
6563 })
6564 })
6565 .collect();
6566 let rows2: Vec<String> = batches2
6567 .iter()
6568 .flat_map(|b| {
6569 (0..b.num_rows()).map(move |r| {
6570 (0..b.num_columns())
6571 .map(|c| {
6572 let col = b.column(c);
6573 format!("{:?}", col.slice(r, 1))
6574 })
6575 .collect::<Vec<_>>()
6576 .join(",")
6577 })
6578 })
6579 .collect();
6580 if rows1.len() != rows2.len() {
6581 return Err(runtime_err(format!(
6582 "assert_table_eq: row count differs ({} vs {})",
6583 rows1.len(),
6584 rows2.len()
6585 )));
6586 }
6587 for (i, (r1, r2)) in rows1.iter().zip(rows2.iter()).enumerate() {
6588 if r1 != r2 {
6589 return Err(runtime_err(format!(
6590 "assert_table_eq: row {} differs\n left: {}\n right: {}",
6591 i, r1, r2
6592 )));
6593 }
6594 }
6595 Ok(VmValue::None)
6596 }
6597 #[cfg(not(feature = "native"))]
6598 BuiltinId::AssertTableEq => Err(runtime_err("assert_table_eq() not available in WASM")),
6599
6600 BuiltinId::Today => {
6602 use chrono::{Datelike, TimeZone};
6603 let now = chrono::Utc::now();
6604 let midnight = chrono::Utc
6605 .with_ymd_and_hms(now.year(), now.month(), now.day(), 0, 0, 0)
6606 .single()
6607 .ok_or_else(|| runtime_err("Failed to compute today"))?;
6608 Ok(VmValue::DateTime(midnight.timestamp_millis()))
6609 }
6610 BuiltinId::DateAdd => {
6611 if args.len() < 3 {
6612 return Err(runtime_err("date_add() expects datetime, amount, unit"));
6613 }
6614 let ms = match &args[0] {
6615 VmValue::DateTime(ms) => *ms,
6616 VmValue::Int(ms) => *ms,
6617 _ => return Err(runtime_err("date_add() first arg must be datetime")),
6618 };
6619 let amount = match &args[1] {
6620 VmValue::Int(n) => *n,
6621 _ => return Err(runtime_err("date_add() amount must be an integer")),
6622 };
6623 let unit = match &args[2] {
6624 VmValue::String(s) => s.as_ref(),
6625 _ => return Err(runtime_err("date_add() unit must be a string")),
6626 };
6627 let offset_ms = match unit {
6628 "second" | "seconds" => amount * 1000,
6629 "minute" | "minutes" => amount * 60 * 1000,
6630 "hour" | "hours" => amount * 3600 * 1000,
6631 "day" | "days" => amount * 86400 * 1000,
6632 "week" | "weeks" => amount * 7 * 86400 * 1000,
6633 _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6634 };
6635 Ok(VmValue::DateTime(ms + offset_ms))
6636 }
6637 BuiltinId::DateDiff => {
6638 if args.len() < 3 {
6639 return Err(runtime_err(
6640 "date_diff() expects datetime1, datetime2, unit",
6641 ));
6642 }
6643 let ms1 = match &args[0] {
6644 VmValue::DateTime(ms) => *ms,
6645 VmValue::Int(ms) => *ms,
6646 _ => return Err(runtime_err("date_diff() args must be datetimes")),
6647 };
6648 let ms2 = match &args[1] {
6649 VmValue::DateTime(ms) => *ms,
6650 VmValue::Int(ms) => *ms,
6651 _ => return Err(runtime_err("date_diff() args must be datetimes")),
6652 };
6653 let unit = match &args[2] {
6654 VmValue::String(s) => s.as_ref(),
6655 _ => return Err(runtime_err("date_diff() unit must be a string")),
6656 };
6657 let diff_ms = ms1 - ms2;
6658 let result = match unit {
6659 "second" | "seconds" => diff_ms / 1000,
6660 "minute" | "minutes" => diff_ms / (60 * 1000),
6661 "hour" | "hours" => diff_ms / (3600 * 1000),
6662 "day" | "days" => diff_ms / (86400 * 1000),
6663 "week" | "weeks" => diff_ms / (7 * 86400 * 1000),
6664 _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6665 };
6666 Ok(VmValue::Int(result))
6667 }
6668 BuiltinId::DateTrunc => {
6669 if args.len() < 2 {
6670 return Err(runtime_err("date_trunc() expects datetime and unit"));
6671 }
6672 let ms = match &args[0] {
6673 VmValue::DateTime(ms) => *ms,
6674 VmValue::Int(ms) => *ms,
6675 _ => return Err(runtime_err("date_trunc() first arg must be datetime")),
6676 };
6677 let unit = match &args[1] {
6678 VmValue::String(s) => s.as_ref(),
6679 _ => return Err(runtime_err("date_trunc() unit must be a string")),
6680 };
6681 use chrono::{Datelike, TimeZone, Timelike};
6682 let secs = ms / 1000;
6683 let dt = chrono::Utc
6684 .timestamp_opt(secs, 0)
6685 .single()
6686 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6687 let truncated = match unit {
6688 "second" => chrono::Utc
6689 .with_ymd_and_hms(
6690 dt.year(),
6691 dt.month(),
6692 dt.day(),
6693 dt.hour(),
6694 dt.minute(),
6695 dt.second(),
6696 )
6697 .single(),
6698 "minute" => chrono::Utc
6699 .with_ymd_and_hms(
6700 dt.year(),
6701 dt.month(),
6702 dt.day(),
6703 dt.hour(),
6704 dt.minute(),
6705 0,
6706 )
6707 .single(),
6708 "hour" => chrono::Utc
6709 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
6710 .single(),
6711 "day" => chrono::Utc
6712 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
6713 .single(),
6714 "month" => chrono::Utc
6715 .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
6716 .single(),
6717 "year" => chrono::Utc
6718 .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
6719 .single(),
6720 _ => return Err(runtime_err(format!("Unknown truncation unit: {unit}"))),
6721 };
6722 Ok(VmValue::DateTime(
6723 truncated
6724 .ok_or_else(|| runtime_err("Invalid truncation"))?
6725 .timestamp_millis(),
6726 ))
6727 }
6728 BuiltinId::DateExtract => {
6729 if args.len() < 2 {
6730 return Err(runtime_err("extract() expects datetime and part"));
6731 }
6732 let ms = match &args[0] {
6733 VmValue::DateTime(ms) => *ms,
6734 VmValue::Int(ms) => *ms,
6735 _ => return Err(runtime_err("extract() first arg must be datetime")),
6736 };
6737 let part = match &args[1] {
6738 VmValue::String(s) => s.as_ref(),
6739 _ => return Err(runtime_err("extract() part must be a string")),
6740 };
6741 use chrono::{Datelike, TimeZone, Timelike};
6742 let secs = ms / 1000;
6743 let dt = chrono::Utc
6744 .timestamp_opt(secs, 0)
6745 .single()
6746 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6747 let val = match part {
6748 "year" => dt.year() as i64,
6749 "month" => dt.month() as i64,
6750 "day" => dt.day() as i64,
6751 "hour" => dt.hour() as i64,
6752 "minute" => dt.minute() as i64,
6753 "second" => dt.second() as i64,
6754 "weekday" | "dow" => dt.weekday().num_days_from_monday() as i64,
6755 "day_of_year" | "doy" => dt.ordinal() as i64,
6756 _ => return Err(runtime_err(format!("Unknown date part: {part}"))),
6757 };
6758 Ok(VmValue::Int(val))
6759 }
6760
6761 #[cfg(feature = "mcp")]
6763 BuiltinId::McpConnect => {
6764 if args.is_empty() {
6765 return Err(runtime_err(
6766 "mcp_connect expects at least 1 argument: command or URL",
6767 ));
6768 }
6769 let command = match &args[0] {
6770 VmValue::String(s) => s.to_string(),
6771 _ => return Err(runtime_err("mcp_connect: first argument must be a string")),
6772 };
6773
6774 #[cfg(feature = "native")]
6776 let sampling_cb: Option<tl_mcp::SamplingCallback> =
6777 Some(Arc::new(|req: tl_mcp::SamplingRequest| {
6778 let model = req
6779 .model_hint
6780 .as_deref()
6781 .unwrap_or("claude-sonnet-4-20250514");
6782 let messages: Vec<serde_json::Value> = req
6783 .messages
6784 .iter()
6785 .map(|(role, content)| {
6786 serde_json::json!({"role": role, "content": content})
6787 })
6788 .collect();
6789 let response = tl_ai::chat_with_tools(
6790 model,
6791 req.system_prompt.as_deref(),
6792 &messages,
6793 &[], None, None, None, )
6798 .map_err(|e| format!("Sampling LLM error: {e}"))?;
6799 match response {
6800 tl_ai::LlmResponse::Text(text) => Ok(tl_mcp::SamplingResponse {
6801 model: model.to_string(),
6802 content: text,
6803 stop_reason: Some("endTurn".to_string()),
6804 }),
6805 tl_ai::LlmResponse::ToolUse(_) => {
6806 Err("Sampling does not support tool use".to_string())
6807 }
6808 }
6809 }));
6810
6811 #[cfg(not(feature = "native"))]
6812 let sampling_cb: Option<tl_mcp::SamplingCallback> = None;
6813
6814 let client = if command.starts_with("http://") || command.starts_with("https://") {
6816 tl_mcp::McpClient::connect_http_with_sampling(&command, sampling_cb)
6817 .map_err(|e| runtime_err(format!("mcp_connect (HTTP) failed: {e}")))?
6818 } else {
6819 let cmd_args: Vec<String> = args[1..]
6820 .iter()
6821 .map(|a| match a {
6822 VmValue::String(s) => s.to_string(),
6823 other => format!("{}", other),
6824 })
6825 .collect();
6826 tl_mcp::McpClient::connect_with_sampling(
6827 &command,
6828 &cmd_args,
6829 self.security_policy.as_ref(),
6830 sampling_cb,
6831 )
6832 .map_err(|e| runtime_err(format!("mcp_connect failed: {e}")))?
6833 };
6834 Ok(VmValue::McpClient(Arc::new(client)))
6835 }
6836 #[cfg(not(feature = "mcp"))]
6837 BuiltinId::McpConnect => {
6838 Err(runtime_err("MCP not available. Build with --features mcp"))
6839 }
6840
6841 #[cfg(feature = "mcp")]
6842 BuiltinId::McpListTools => {
6843 if args.is_empty() {
6844 return Err(runtime_err("mcp_list_tools expects 1 argument: client"));
6845 }
6846 match &args[0] {
6847 VmValue::McpClient(client) => {
6848 let tools = client
6849 .list_tools()
6850 .map_err(|e| runtime_err(format!("mcp_list_tools failed: {e}")))?;
6851 let tool_values: Vec<VmValue> = tools
6852 .iter()
6853 .map(|tool| {
6854 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6855 pairs.push((
6856 Arc::from("name"),
6857 VmValue::String(Arc::from(tool.name.as_ref())),
6858 ));
6859 if let Some(desc) = &tool.description {
6860 pairs.push((
6861 Arc::from("description"),
6862 VmValue::String(Arc::from(desc.as_ref())),
6863 ));
6864 }
6865 let schema_json = serde_json::to_string(tool.input_schema.as_ref())
6866 .unwrap_or_default();
6867 if !schema_json.is_empty() && schema_json != "{}" {
6868 pairs.push((
6869 Arc::from("input_schema"),
6870 VmValue::String(Arc::from(schema_json.as_str())),
6871 ));
6872 }
6873 VmValue::Map(Box::new(pairs))
6874 })
6875 .collect();
6876 Ok(VmValue::List(Box::new(tool_values)))
6877 }
6878 _ => Err(runtime_err(
6879 "mcp_list_tools: argument must be an mcp_client",
6880 )),
6881 }
6882 }
6883 #[cfg(not(feature = "mcp"))]
6884 BuiltinId::McpListTools => {
6885 Err(runtime_err("MCP not available. Build with --features mcp"))
6886 }
6887
6888 #[cfg(feature = "mcp")]
6889 BuiltinId::McpCallTool => {
6890 if args.len() < 2 {
6891 return Err(runtime_err(
6892 "mcp_call_tool expects 2-3 arguments: client, tool_name, [args]",
6893 ));
6894 }
6895 let client = match &args[0] {
6896 VmValue::McpClient(c) => c.clone(),
6897 _ => {
6898 return Err(runtime_err(
6899 "mcp_call_tool: first argument must be an mcp_client",
6900 ));
6901 }
6902 };
6903 let tool_name = match &args[1] {
6904 VmValue::String(s) => s.to_string(),
6905 _ => return Err(runtime_err("mcp_call_tool: tool_name must be a string")),
6906 };
6907 let arguments = if args.len() > 2 {
6908 vm_value_to_json(&args[2])
6909 } else {
6910 serde_json::Value::Object(serde_json::Map::new())
6911 };
6912 let result = client
6913 .call_tool(&tool_name, arguments)
6914 .map_err(|e| runtime_err(format!("mcp_call_tool failed: {e}")))?;
6915 let mut content_parts: Vec<VmValue> = Vec::new();
6916 for content in &result.content {
6917 if let Some(text) = content.as_text() {
6918 content_parts.push(VmValue::String(Arc::from(text.text.as_str())));
6919 }
6920 }
6921 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6922 if content_parts.len() == 1 {
6923 pairs.push((
6924 Arc::from("content"),
6925 content_parts.into_iter().next().unwrap(),
6926 ));
6927 } else {
6928 pairs.push((Arc::from("content"), VmValue::List(Box::new(content_parts))));
6929 }
6930 pairs.push((
6931 Arc::from("is_error"),
6932 VmValue::Bool(result.is_error.unwrap_or(false)),
6933 ));
6934 Ok(VmValue::Map(Box::new(pairs)))
6935 }
6936 #[cfg(not(feature = "mcp"))]
6937 BuiltinId::McpCallTool => {
6938 Err(runtime_err("MCP not available. Build with --features mcp"))
6939 }
6940
6941 #[cfg(feature = "mcp")]
6942 BuiltinId::McpDisconnect => {
6943 if args.is_empty() {
6944 return Err(runtime_err("mcp_disconnect expects 1 argument: client"));
6945 }
6946 match &args[0] {
6947 VmValue::McpClient(_) => Ok(VmValue::None),
6948 _ => Err(runtime_err(
6949 "mcp_disconnect: argument must be an mcp_client",
6950 )),
6951 }
6952 }
6953 #[cfg(not(feature = "mcp"))]
6954 BuiltinId::McpDisconnect => {
6955 Err(runtime_err("MCP not available. Build with --features mcp"))
6956 }
6957
6958 #[cfg(feature = "mcp")]
6959 BuiltinId::McpPing => {
6960 if args.is_empty() {
6961 return Err(runtime_err("mcp_ping expects 1 argument: client"));
6962 }
6963 match &args[0] {
6964 VmValue::McpClient(client) => {
6965 client
6966 .ping()
6967 .map_err(|e| runtime_err(format!("mcp_ping failed: {e}")))?;
6968 Ok(VmValue::Bool(true))
6969 }
6970 _ => Err(runtime_err("mcp_ping: argument must be an mcp_client")),
6971 }
6972 }
6973 #[cfg(not(feature = "mcp"))]
6974 BuiltinId::McpPing => Err(runtime_err("MCP not available. Build with --features mcp")),
6975
6976 #[cfg(feature = "mcp")]
6977 BuiltinId::McpServerInfo => {
6978 if args.is_empty() {
6979 return Err(runtime_err("mcp_server_info expects 1 argument: client"));
6980 }
6981 match &args[0] {
6982 VmValue::McpClient(client) => match client.server_info() {
6983 Some(info) => {
6984 let pairs: Vec<(Arc<str>, VmValue)> = vec![
6985 (
6986 Arc::from("name"),
6987 VmValue::String(Arc::from(info.server_info.name.as_str())),
6988 ),
6989 (
6990 Arc::from("version"),
6991 VmValue::String(Arc::from(info.server_info.version.as_str())),
6992 ),
6993 ];
6994 Ok(VmValue::Map(Box::new(pairs)))
6995 }
6996 None => Ok(VmValue::None),
6997 },
6998 _ => Err(runtime_err(
6999 "mcp_server_info: argument must be an mcp_client",
7000 )),
7001 }
7002 }
7003 #[cfg(not(feature = "mcp"))]
7004 BuiltinId::McpServerInfo => {
7005 Err(runtime_err("MCP not available. Build with --features mcp"))
7006 }
7007
7008 #[cfg(feature = "mcp")]
7009 BuiltinId::McpServe => {
7010 self.check_permission("network")?;
7011 if args.is_empty() {
7012 return Err(runtime_err(
7013 "mcp_serve expects 1 argument: list of tool definitions",
7014 ));
7015 }
7016 let tool_list = match &args[0] {
7017 VmValue::List(items) => items.as_ref().clone(),
7018 _ => {
7019 return Err(runtime_err(
7020 "mcp_serve: argument must be a list of tool maps",
7021 ));
7022 }
7023 };
7024
7025 let mut channel_tools = Vec::new();
7027 let mut tool_handlers: HashMap<String, VmValue> = HashMap::new();
7028
7029 for item in &tool_list {
7030 let pairs = match item {
7031 VmValue::Map(p) => p.as_ref(),
7032 _ => {
7033 return Err(runtime_err(
7034 "mcp_serve: each tool must be a map with name, description, handler",
7035 ));
7036 }
7037 };
7038 let mut name = String::new();
7039 let mut description = String::new();
7040 let mut handler = None;
7041 let mut input_schema = serde_json::json!({"type": "object"});
7042
7043 for (k, v) in pairs {
7044 match k.as_ref() {
7045 "name" => {
7046 if let VmValue::String(s) = v {
7047 name = s.to_string();
7048 }
7049 }
7050 "description" => {
7051 if let VmValue::String(s) = v {
7052 description = s.to_string();
7053 }
7054 }
7055 "handler" => {
7056 handler = Some(v.clone());
7057 }
7058 "input_schema" | "parameters" => {
7059 if let VmValue::String(s) = v
7060 && let Ok(parsed) =
7061 serde_json::from_str::<serde_json::Value>(s.as_ref())
7062 {
7063 input_schema = parsed;
7064 }
7065 }
7066 _ => {}
7067 }
7068 }
7069
7070 if name.is_empty() {
7071 return Err(runtime_err("mcp_serve: tool missing 'name'"));
7072 }
7073 if let Some(h) = handler {
7074 tool_handlers.insert(name.clone(), h);
7075 }
7076
7077 channel_tools.push(tl_mcp::server::ChannelToolDef {
7078 name,
7079 description,
7080 input_schema,
7081 });
7082 }
7083
7084 let (builder, rx) = tl_mcp::server::TlServerHandler::builder()
7086 .name("tl-mcp-server")
7087 .version("1.0.0")
7088 .channel_tools(channel_tools);
7089 let server_handler = builder.build();
7090
7091 let _server_handle = tl_mcp::server::serve_stdio_background(server_handler);
7093
7094 while let Ok(req) = rx.recv() {
7096 let result = if let Some(func) = tool_handlers.get(&req.tool_name) {
7097 let call_args = self.json_to_vm_args(&req.arguments);
7099 match self.call_value(func.clone(), &call_args) {
7100 Ok(val) => {
7101 Ok(serde_json::json!(format!("{val}")))
7103 }
7104 Err(e) => Err(format!("{e}")),
7105 }
7106 } else {
7107 Err(format!("Unknown tool: {}", req.tool_name))
7108 };
7109 let _ = req.response_tx.send(result);
7110 }
7111
7112 Ok(VmValue::None)
7113 }
7114 #[cfg(not(feature = "mcp"))]
7115 BuiltinId::McpServe => Err(runtime_err("MCP not available. Build with --features mcp")),
7116
7117 #[cfg(feature = "mcp")]
7119 BuiltinId::McpListResources => {
7120 if args.is_empty() {
7121 return Err(runtime_err("mcp_list_resources expects 1 argument: client"));
7122 }
7123 match &args[0] {
7124 VmValue::McpClient(client) => {
7125 let resources = client
7126 .list_resources()
7127 .map_err(|e| runtime_err(format!("mcp_list_resources failed: {e}")))?;
7128 let vals: Vec<VmValue> = resources
7129 .iter()
7130 .map(|r| {
7131 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7132 pairs.push((
7133 Arc::from("uri"),
7134 VmValue::String(Arc::from(r.uri.as_str())),
7135 ));
7136 pairs.push((
7137 Arc::from("name"),
7138 VmValue::String(Arc::from(r.name.as_str())),
7139 ));
7140 if let Some(desc) = &r.description {
7141 pairs.push((
7142 Arc::from("description"),
7143 VmValue::String(Arc::from(desc.as_str())),
7144 ));
7145 }
7146 if let Some(mime) = &r.mime_type {
7147 pairs.push((
7148 Arc::from("mime_type"),
7149 VmValue::String(Arc::from(mime.as_str())),
7150 ));
7151 }
7152 VmValue::Map(Box::new(pairs))
7153 })
7154 .collect();
7155 Ok(VmValue::List(Box::new(vals)))
7156 }
7157 _ => Err(runtime_err(
7158 "mcp_list_resources: argument must be an mcp_client",
7159 )),
7160 }
7161 }
7162 #[cfg(not(feature = "mcp"))]
7163 BuiltinId::McpListResources => {
7164 Err(runtime_err("MCP not available. Build with --features mcp"))
7165 }
7166
7167 #[cfg(feature = "mcp")]
7168 BuiltinId::McpReadResource => {
7169 if args.len() < 2 {
7170 return Err(runtime_err(
7171 "mcp_read_resource expects 2 arguments: client, uri",
7172 ));
7173 }
7174 let client = match &args[0] {
7175 VmValue::McpClient(c) => c.clone(),
7176 _ => {
7177 return Err(runtime_err(
7178 "mcp_read_resource: first argument must be an mcp_client",
7179 ));
7180 }
7181 };
7182 let uri = match &args[1] {
7183 VmValue::String(s) => s.to_string(),
7184 _ => return Err(runtime_err("mcp_read_resource: uri must be a string")),
7185 };
7186 let result = client
7187 .read_resource(&uri)
7188 .map_err(|e| runtime_err(format!("mcp_read_resource failed: {e}")))?;
7189 let contents: Vec<VmValue> = result
7191 .contents
7192 .iter()
7193 .map(|content| {
7194 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7195 let json = serde_json::to_value(content).unwrap_or_default();
7196 if let Some(uri_s) = json.get("uri").and_then(|v| v.as_str()) {
7197 pairs.push((Arc::from("uri"), VmValue::String(Arc::from(uri_s))));
7198 }
7199 if let Some(mime) = json.get("mimeType").and_then(|v| v.as_str()) {
7200 pairs.push((Arc::from("mime_type"), VmValue::String(Arc::from(mime))));
7201 }
7202 if let Some(text) = json.get("text").and_then(|v| v.as_str()) {
7203 pairs.push((Arc::from("text"), VmValue::String(Arc::from(text))));
7204 }
7205 if let Some(blob) = json.get("blob").and_then(|v| v.as_str()) {
7206 pairs.push((Arc::from("blob"), VmValue::String(Arc::from(blob))));
7207 }
7208 VmValue::Map(Box::new(pairs))
7209 })
7210 .collect();
7211 if contents.len() == 1 {
7212 Ok(contents.into_iter().next().unwrap())
7213 } else {
7214 Ok(VmValue::List(Box::new(contents)))
7215 }
7216 }
7217 #[cfg(not(feature = "mcp"))]
7218 BuiltinId::McpReadResource => {
7219 Err(runtime_err("MCP not available. Build with --features mcp"))
7220 }
7221
7222 #[cfg(feature = "mcp")]
7223 BuiltinId::McpListPrompts => {
7224 if args.is_empty() {
7225 return Err(runtime_err("mcp_list_prompts expects 1 argument: client"));
7226 }
7227 match &args[0] {
7228 VmValue::McpClient(client) => {
7229 let prompts = client
7230 .list_prompts()
7231 .map_err(|e| runtime_err(format!("mcp_list_prompts failed: {e}")))?;
7232 let vals: Vec<VmValue> = prompts
7233 .iter()
7234 .map(|p| {
7235 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7236 pairs.push((
7237 Arc::from("name"),
7238 VmValue::String(Arc::from(p.name.as_str())),
7239 ));
7240 if let Some(desc) = &p.description {
7241 pairs.push((
7242 Arc::from("description"),
7243 VmValue::String(Arc::from(desc.as_str())),
7244 ));
7245 }
7246 if let Some(prompt_args) = &p.arguments {
7247 let arg_vals: Vec<VmValue> = prompt_args
7248 .iter()
7249 .map(|a| {
7250 let mut arg_pairs: Vec<(Arc<str>, VmValue)> =
7251 Vec::new();
7252 arg_pairs.push((
7253 Arc::from("name"),
7254 VmValue::String(Arc::from(a.name.as_str())),
7255 ));
7256 if let Some(desc) = &a.description {
7257 arg_pairs.push((
7258 Arc::from("description"),
7259 VmValue::String(Arc::from(desc.as_str())),
7260 ));
7261 }
7262 arg_pairs.push((
7263 Arc::from("required"),
7264 VmValue::Bool(a.required.unwrap_or(false)),
7265 ));
7266 VmValue::Map(Box::new(arg_pairs))
7267 })
7268 .collect();
7269 pairs.push((
7270 Arc::from("arguments"),
7271 VmValue::List(Box::new(arg_vals)),
7272 ));
7273 }
7274 VmValue::Map(Box::new(pairs))
7275 })
7276 .collect();
7277 Ok(VmValue::List(Box::new(vals)))
7278 }
7279 _ => Err(runtime_err(
7280 "mcp_list_prompts: argument must be an mcp_client",
7281 )),
7282 }
7283 }
7284 #[cfg(not(feature = "mcp"))]
7285 BuiltinId::McpListPrompts => {
7286 Err(runtime_err("MCP not available. Build with --features mcp"))
7287 }
7288
7289 #[cfg(feature = "mcp")]
7290 BuiltinId::McpGetPrompt => {
7291 if args.len() < 2 {
7292 return Err(runtime_err(
7293 "mcp_get_prompt expects 2-3 arguments: client, name, [args]",
7294 ));
7295 }
7296 let client = match &args[0] {
7297 VmValue::McpClient(c) => c.clone(),
7298 _ => {
7299 return Err(runtime_err(
7300 "mcp_get_prompt: first argument must be an mcp_client",
7301 ));
7302 }
7303 };
7304 let name = match &args[1] {
7305 VmValue::String(s) => s.to_string(),
7306 _ => return Err(runtime_err("mcp_get_prompt: name must be a string")),
7307 };
7308 let prompt_args = if args.len() > 2 {
7309 let json = vm_value_to_json(&args[2]);
7310 json.as_object().cloned()
7311 } else {
7312 None
7313 };
7314 let result = client
7315 .get_prompt(&name, prompt_args)
7316 .map_err(|e| runtime_err(format!("mcp_get_prompt failed: {e}")))?;
7317 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7318 if let Some(desc) = &result.description {
7319 pairs.push((
7320 Arc::from("description"),
7321 VmValue::String(Arc::from(desc.as_str())),
7322 ));
7323 }
7324 let messages: Vec<VmValue> = result
7326 .messages
7327 .iter()
7328 .map(|m| {
7329 let mut msg_pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7330 let msg_json = serde_json::to_value(m).unwrap_or_default();
7331 if let Some(role) = msg_json.get("role").and_then(|v| v.as_str()) {
7333 msg_pairs.push((Arc::from("role"), VmValue::String(Arc::from(role))));
7334 }
7335 if let Some(content) = msg_json.get("content") {
7337 if let Some(text) = content.get("text").and_then(|v| v.as_str()) {
7338 msg_pairs
7339 .push((Arc::from("content"), VmValue::String(Arc::from(text))));
7340 } else {
7341 let content_str = content.to_string();
7342 msg_pairs.push((
7343 Arc::from("content"),
7344 VmValue::String(Arc::from(content_str.as_str())),
7345 ));
7346 }
7347 }
7348 VmValue::Map(Box::new(msg_pairs))
7349 })
7350 .collect();
7351 pairs.push((Arc::from("messages"), VmValue::List(Box::new(messages))));
7352 Ok(VmValue::Map(Box::new(pairs)))
7353 }
7354 #[cfg(not(feature = "mcp"))]
7355 BuiltinId::McpGetPrompt => {
7356 Err(runtime_err("MCP not available. Build with --features mcp"))
7357 }
7358 }
7359 }
7360
7361 fn vmvalue_to_f64_list(&self, val: &VmValue) -> Result<Vec<f64>, TlError> {
7364 match val {
7365 VmValue::List(items) => items
7366 .iter()
7367 .map(|item| match item {
7368 VmValue::Int(n) => Ok(*n as f64),
7369 VmValue::Float(f) => Ok(*f),
7370 _ => Err(runtime_err("Expected number in list")),
7371 })
7372 .collect(),
7373 VmValue::Int(n) => Ok(vec![*n as f64]),
7374 VmValue::Float(f) => Ok(vec![*f]),
7375 _ => Err(runtime_err("Expected a list of numbers")),
7376 }
7377 }
7378
7379 fn vmvalue_to_usize_list(&self, val: &VmValue) -> Result<Vec<usize>, TlError> {
7380 match val {
7381 VmValue::List(items) => items
7382 .iter()
7383 .map(|item| match item {
7384 VmValue::Int(n) => Ok(*n as usize),
7385 _ => Err(runtime_err("Expected integer in shape list")),
7386 })
7387 .collect(),
7388 _ => Err(runtime_err("Expected a list of integers for shape")),
7389 }
7390 }
7391
7392 #[cfg(feature = "native")]
7393 fn handle_train(
7394 &mut self,
7395 frame_idx: usize,
7396 algo_const: u8,
7397 config_const: u8,
7398 ) -> Result<VmValue, TlError> {
7399 let frame = &self.frames[frame_idx];
7400 let algorithm = match &frame.prototype.constants[algo_const as usize] {
7401 Constant::String(s) => s.to_string(),
7402 _ => return Err(runtime_err("Expected string constant for algorithm")),
7403 };
7404 let config_args = match &frame.prototype.constants[config_const as usize] {
7405 Constant::AstExprList(args) => args.clone(),
7406 _ => return Err(runtime_err("Expected AST expr list for train config")),
7407 };
7408
7409 let mut data_val = None;
7411 let mut target_name = None;
7412 let mut feature_names: Vec<String> = Vec::new();
7413
7414 for arg in &config_args {
7415 if let AstExpr::NamedArg { name, value } = arg {
7416 match name.as_str() {
7417 "data" => {
7418 data_val = Some(self.eval_ast_to_vm(value)?);
7419 }
7420 "target" => {
7421 if let AstExpr::String(s) = value.as_ref() {
7422 target_name = Some(s.clone());
7423 }
7424 }
7425 "features" => {
7426 if let AstExpr::List(items) = value.as_ref() {
7427 for item in items {
7428 if let AstExpr::String(s) = item {
7429 feature_names.push(s.clone());
7430 }
7431 }
7432 }
7433 }
7434 _ => {}
7435 }
7436 }
7437 }
7438
7439 let table = match data_val {
7441 Some(VmValue::Table(t)) => t,
7442 _ => return Err(runtime_err("train: data must be a table")),
7443 };
7444 let target = target_name.ok_or_else(|| runtime_err("train: target is required"))?;
7445
7446 let batches = self.engine().collect(table.df).map_err(runtime_err)?;
7448 if batches.is_empty() {
7449 return Err(runtime_err("train: empty dataset"));
7450 }
7451
7452 let batch = &batches[0];
7454 let schema = batch.schema();
7455 if feature_names.is_empty() {
7456 for field in schema.fields() {
7457 if field.name() != &target {
7458 feature_names.push(field.name().clone());
7459 }
7460 }
7461 }
7462
7463 let n_rows = batch.num_rows();
7465 let n_features = feature_names.len();
7466 let mut features_data = Vec::with_capacity(n_rows * n_features);
7467 let mut target_data = Vec::with_capacity(n_rows);
7468
7469 for col_name in &feature_names {
7470 let col_idx = schema
7471 .index_of(col_name)
7472 .map_err(|_| runtime_err(format!("Column not found: {col_name}")))?;
7473 let col_arr = batch.column(col_idx);
7474 Self::extract_f64_column(col_arr, &mut features_data)?;
7475 }
7476
7477 let target_idx = schema
7479 .index_of(&target)
7480 .map_err(|_| runtime_err(format!("Target column not found: {target}")))?;
7481 let target_arr = batch.column(target_idx);
7482 Self::extract_f64_column(target_arr, &mut target_data)?;
7483
7484 let mut row_major = Vec::with_capacity(n_rows * n_features);
7486 for row in 0..n_rows {
7487 for col in 0..n_features {
7488 row_major.push(features_data[col * n_rows + row]);
7489 }
7490 }
7491
7492 let features_tensor = tl_ai::TlTensor::from_vec(row_major, &[n_rows, n_features])
7493 .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7494 let target_tensor = tl_ai::TlTensor::from_vec(target_data, &[n_rows])
7495 .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7496
7497 let config = tl_ai::TrainConfig {
7498 features: features_tensor,
7499 target: target_tensor,
7500 feature_names: feature_names.clone(),
7501 target_name: target.clone(),
7502 model_name: algorithm.clone(),
7503 split_ratio: 0.8,
7504 hyperparams: std::collections::HashMap::new(),
7505 };
7506
7507 let model = tl_ai::train(&algorithm, &config)
7508 .map_err(|e| runtime_err(format!("Training failed: {e}")))?;
7509
7510 Ok(VmValue::Model(Arc::new(model)))
7511 }
7512
7513 #[cfg(feature = "native")]
7514 fn extract_f64_column(
7515 col: &std::sync::Arc<dyn tl_data::datafusion::arrow::array::Array>,
7516 out: &mut Vec<f64>,
7517 ) -> Result<(), TlError> {
7518 use tl_data::datafusion::arrow::array::{
7519 Array, Float32Array, Float64Array, Int32Array, Int64Array,
7520 };
7521 let len = col.len();
7522 if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
7523 for i in 0..len {
7524 out.push(if arr.is_null(i) { 0.0 } else { arr.value(i) });
7525 }
7526 } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
7527 for i in 0..len {
7528 out.push(if arr.is_null(i) {
7529 0.0
7530 } else {
7531 arr.value(i) as f64
7532 });
7533 }
7534 } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
7535 for i in 0..len {
7536 out.push(if arr.is_null(i) {
7537 0.0
7538 } else {
7539 arr.value(i) as f64
7540 });
7541 }
7542 } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
7543 for i in 0..len {
7544 out.push(if arr.is_null(i) {
7545 0.0
7546 } else {
7547 arr.value(i) as f64
7548 });
7549 }
7550 } else {
7551 return Err(runtime_err(
7552 "Column must be numeric (int32, int64, float32, float64)",
7553 ));
7554 }
7555 Ok(())
7556 }
7557
7558 #[cfg(feature = "native")]
7559 fn handle_pipeline_exec(
7560 &mut self,
7561 frame_idx: usize,
7562 name_const: u8,
7563 config_const: u8,
7564 ) -> Result<VmValue, TlError> {
7565 let frame = &self.frames[frame_idx];
7566 let name = match &frame.prototype.constants[name_const as usize] {
7567 Constant::String(s) => s.to_string(),
7568 _ => return Err(runtime_err("Expected string constant for pipeline name")),
7569 };
7570
7571 let mut schedule = None;
7572 let mut timeout_ms = None;
7573 let mut retries = 0u32;
7574
7575 if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7576 for arg in args {
7577 if let AstExpr::NamedArg { name: key, value } = arg {
7578 match key.as_str() {
7579 "schedule" => {
7580 if let AstExpr::String(s) = value.as_ref() {
7581 schedule = Some(s.clone());
7582 }
7583 }
7584 "timeout" => {
7585 if let AstExpr::String(s) = value.as_ref() {
7586 timeout_ms = tl_stream::parse_duration(s).ok();
7587 }
7588 }
7589 "retries" => {
7590 if let AstExpr::Int(n) = value.as_ref() {
7591 retries = *n as u32;
7592 }
7593 }
7594 _ => {}
7595 }
7596 }
7597 }
7598 }
7599
7600 let def = tl_stream::PipelineDef {
7601 name,
7602 schedule,
7603 timeout_ms,
7604 retries,
7605 };
7606
7607 self.output
7608 .push(format!("Pipeline '{}': success", def.name));
7609 Ok(VmValue::PipelineDef(Arc::new(def)))
7610 }
7611
7612 #[cfg(feature = "native")]
7613 fn handle_stream_exec(
7614 &mut self,
7615 frame_idx: usize,
7616 config_const: u8,
7617 ) -> Result<VmValue, TlError> {
7618 let frame = &self.frames[frame_idx];
7619 let config_args = match &frame.prototype.constants[config_const as usize] {
7620 Constant::AstExprList(args) => args.clone(),
7621 _ => return Err(runtime_err("Expected AST expr list for stream config")),
7622 };
7623
7624 let mut name = String::new();
7625 let mut window = None;
7626 let mut watermark_ms = None;
7627
7628 for arg in &config_args {
7629 if let AstExpr::NamedArg { name: key, value } = arg {
7630 match key.as_str() {
7631 "name" => {
7632 if let AstExpr::String(s) = value.as_ref() {
7633 name = s.clone();
7634 }
7635 }
7636 "window" => {
7637 if let AstExpr::String(s) = value.as_ref() {
7638 window = Self::parse_window_type(s);
7639 }
7640 }
7641 "watermark" => {
7642 if let AstExpr::String(s) = value.as_ref() {
7643 watermark_ms = tl_stream::parse_duration(s).ok();
7644 }
7645 }
7646 _ => {}
7647 }
7648 }
7649 }
7650
7651 let def = tl_stream::StreamDef {
7652 name: name.clone(),
7653 window,
7654 watermark_ms,
7655 };
7656
7657 self.output.push(format!("Stream '{}' declared", name));
7658 Ok(VmValue::StreamDef(Arc::new(def)))
7659 }
7660
7661 #[cfg(feature = "native")]
7662 fn handle_agent_exec(
7663 &mut self,
7664 frame_idx: usize,
7665 name_const: u8,
7666 config_const: u8,
7667 ) -> Result<VmValue, TlError> {
7668 let frame = &self.frames[frame_idx];
7669 let name = match &frame.prototype.constants[name_const as usize] {
7670 Constant::String(s) => s.to_string(),
7671 _ => return Err(runtime_err("Expected string constant for agent name")),
7672 };
7673
7674 let mut model = String::new();
7675 let mut system_prompt = None;
7676 let mut max_turns = 10u32;
7677 let mut temperature = None;
7678 let mut max_tokens = None;
7679 let mut base_url = None;
7680 let mut api_key = None;
7681 let mut output_format = None;
7682 let mut tools = Vec::new();
7683 #[cfg(feature = "mcp")]
7684 let mut mcp_clients: Vec<Arc<tl_mcp::McpClient>> = Vec::new();
7685
7686 if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7687 for arg in args {
7688 if let AstExpr::NamedArg { name: key, value } = arg {
7689 if let Some(tool_name) = key.strip_prefix("tool:") {
7690 let (desc, params) = Self::extract_tool_from_ast(value);
7692 tools.push(tl_stream::AgentTool {
7693 name: tool_name.to_string(),
7694 description: desc,
7695 parameters: params,
7696 });
7697 } else if key.starts_with("mcp_server:") {
7698 #[cfg(feature = "mcp")]
7700 if let AstExpr::Ident(var_name) = value.as_ref()
7701 && let Some(VmValue::McpClient(client)) = self.globals.get(var_name)
7702 {
7703 mcp_clients.push(client.clone());
7704 }
7705 } else {
7706 match key.as_str() {
7707 "model" => {
7708 if let AstExpr::String(s) = value.as_ref() {
7709 model = s.clone();
7710 }
7711 }
7712 "system" => {
7713 if let AstExpr::String(s) = value.as_ref() {
7714 system_prompt = Some(s.clone());
7715 }
7716 }
7717 "max_turns" => {
7718 if let AstExpr::Int(n) = value.as_ref() {
7719 max_turns = *n as u32;
7720 }
7721 }
7722 "temperature" => {
7723 if let AstExpr::Float(f) = value.as_ref() {
7724 temperature = Some(*f);
7725 }
7726 }
7727 "max_tokens" => {
7728 if let AstExpr::Int(n) = value.as_ref() {
7729 max_tokens = Some(*n as u32);
7730 }
7731 }
7732 "base_url" => {
7733 if let AstExpr::String(s) = value.as_ref() {
7734 base_url = Some(s.clone());
7735 }
7736 }
7737 "api_key" => {
7738 if let AstExpr::String(s) = value.as_ref() {
7739 api_key = Some(s.clone());
7740 }
7741 }
7742 "output_format" => {
7743 if let AstExpr::String(s) = value.as_ref() {
7744 output_format = Some(s.clone());
7745 }
7746 }
7747 _ => {}
7748 }
7749 }
7750 }
7751 }
7752 }
7753
7754 let def = tl_stream::AgentDef {
7755 name: name.clone(),
7756 model,
7757 system_prompt,
7758 tools,
7759 max_turns,
7760 temperature,
7761 max_tokens,
7762 base_url,
7763 api_key,
7764 output_format,
7765 };
7766
7767 #[cfg(feature = "mcp")]
7769 if !mcp_clients.is_empty() {
7770 self.mcp_agent_clients.insert(name.clone(), mcp_clients);
7771 }
7772
7773 Ok(VmValue::AgentDef(Arc::new(def)))
7774 }
7775
7776 #[cfg(feature = "native")]
7777 fn extract_tool_from_ast(expr: &AstExpr) -> (String, serde_json::Value) {
7778 let mut desc = String::new();
7779 let mut params = serde_json::Value::Object(serde_json::Map::new());
7780 if let AstExpr::Map(pairs) = expr {
7781 for (key_expr, val_expr) in pairs {
7782 if let AstExpr::Ident(key) | AstExpr::String(key) = key_expr {
7783 match key.as_str() {
7784 "description" => {
7785 if let AstExpr::String(s) = val_expr {
7786 desc = s.clone();
7787 }
7788 }
7789 "parameters" => {
7790 params = Self::ast_to_json(val_expr);
7791 }
7792 _ => {}
7793 }
7794 }
7795 }
7796 }
7797 (desc, params)
7798 }
7799
7800 #[cfg(feature = "native")]
7801 fn ast_to_json(expr: &AstExpr) -> serde_json::Value {
7802 match expr {
7803 AstExpr::String(s) => serde_json::Value::String(s.clone()),
7804 AstExpr::Int(n) => serde_json::json!(*n),
7805 AstExpr::Float(f) => serde_json::json!(*f),
7806 AstExpr::Bool(b) => serde_json::Value::Bool(*b),
7807 AstExpr::None => serde_json::Value::Null,
7808 AstExpr::List(items) => {
7809 serde_json::Value::Array(items.iter().map(Self::ast_to_json).collect())
7810 }
7811 AstExpr::Map(pairs) => {
7812 let mut map = serde_json::Map::new();
7813 for (k, v) in pairs {
7814 let key = match k {
7815 AstExpr::String(s) | AstExpr::Ident(s) => s.clone(),
7816 _ => format!("{k:?}"),
7817 };
7818 map.insert(key, Self::ast_to_json(v));
7819 }
7820 serde_json::Value::Object(map)
7821 }
7822 _ => serde_json::Value::Null,
7823 }
7824 }
7825
7826 #[cfg(feature = "native")]
7827 fn exec_agent_loop(
7828 &mut self,
7829 agent_def: &tl_stream::AgentDef,
7830 user_message: &str,
7831 history: Option<&[(String, String)]>,
7832 ) -> Result<VmValue, TlError> {
7833 use tl_ai::{LlmResponse, chat_with_tools, format_tool_result_messages};
7834
7835 let model = &agent_def.model;
7836 let system = agent_def.system_prompt.as_deref();
7837 let base_url = agent_def.base_url.as_deref();
7838 let api_key = agent_def.api_key.as_deref();
7839
7840 let provider = if model.starts_with("claude") {
7841 "anthropic"
7842 } else {
7843 "openai"
7844 };
7845
7846 #[allow(unused_mut)]
7848 let mut tools_json: Vec<serde_json::Value> = agent_def
7849 .tools
7850 .iter()
7851 .map(|t| {
7852 serde_json::json!({
7853 "type": "function",
7854 "function": {
7855 "name": t.name,
7856 "description": t.description,
7857 "parameters": t.parameters
7858 }
7859 })
7860 })
7861 .collect();
7862
7863 #[cfg(feature = "mcp")]
7865 let mcp_clients = self
7866 .mcp_agent_clients
7867 .get(&agent_def.name)
7868 .cloned()
7869 .unwrap_or_default();
7870 #[cfg(feature = "mcp")]
7871 let mcp_tool_dispatch: std::collections::HashMap<String, usize> = {
7872 let mut dispatch = std::collections::HashMap::new();
7873 for (client_idx, client) in mcp_clients.iter().enumerate() {
7874 if let Ok(mcp_tools) = client.list_tools() {
7875 for tool in mcp_tools {
7876 let tool_name = tool.name.to_string();
7877 tools_json.push(serde_json::json!({
7878 "type": "function",
7879 "function": {
7880 "name": &tool_name,
7881 "description": tool.description.as_deref().unwrap_or(""),
7882 "parameters": serde_json::Value::Object((*tool.input_schema).clone())
7883 }
7884 }));
7885 dispatch.insert(tool_name, client_idx);
7886 }
7887 }
7888 }
7889 dispatch
7890 };
7891
7892 let mut messages: Vec<serde_json::Value> = Vec::new();
7894 if let Some(hist) = history {
7895 for (role, content) in hist {
7896 messages.push(serde_json::json!({"role": role, "content": content}));
7897 }
7898 }
7899 messages.push(serde_json::json!({
7901 "role": "user",
7902 "content": user_message
7903 }));
7904
7905 for turn in 0..agent_def.max_turns {
7906 let response = chat_with_tools(
7907 model,
7908 system,
7909 &messages,
7910 &tools_json,
7911 base_url,
7912 api_key,
7913 agent_def.output_format.as_deref(),
7914 )
7915 .map_err(|e| runtime_err(format!("Agent LLM error: {e}")))?;
7916
7917 match response {
7918 LlmResponse::Text(text) => {
7919 messages.push(serde_json::json!({"role": "assistant", "content": &text}));
7921
7922 let history_list: Vec<VmValue> = messages
7924 .iter()
7925 .filter_map(|m| {
7926 let role = m["role"].as_str()?;
7927 let content = m["content"].as_str()?;
7928 Some(VmValue::List(Box::new(vec![
7929 VmValue::String(Arc::from(role)),
7930 VmValue::String(Arc::from(content)),
7931 ])))
7932 })
7933 .collect();
7934
7935 let result = VmValue::Map(Box::new(vec![
7937 (
7938 Arc::from("response"),
7939 VmValue::String(Arc::from(text.as_str())),
7940 ),
7941 (Arc::from("turns"), VmValue::Int(turn as i64 + 1)),
7942 (Arc::from("history"), VmValue::List(Box::new(history_list))),
7943 ]));
7944
7945 let hook_name = format!("__agent_{}_on_complete__", agent_def.name);
7947 if let Some(hook) = self.globals.get(&hook_name).cloned() {
7948 let _ = self.call_value(hook, std::slice::from_ref(&result));
7949 }
7950
7951 return Ok(result);
7952 }
7953 LlmResponse::ToolUse(tool_calls) => {
7954 let tc_json: Vec<serde_json::Value> = tool_calls
7956 .iter()
7957 .map(|tc| {
7958 serde_json::json!({
7959 "id": tc.id,
7960 "type": "function",
7961 "function": {
7962 "name": tc.name,
7963 "arguments": serde_json::to_string(&tc.input).unwrap_or_default()
7964 }
7965 })
7966 })
7967 .collect();
7968 messages.push(serde_json::json!({
7969 "role": "assistant",
7970 "tool_calls": tc_json
7971 }));
7972
7973 #[allow(unused_mut)]
7975 let mut declared: Vec<String> =
7976 agent_def.tools.iter().map(|t| t.name.clone()).collect();
7977 #[cfg(feature = "mcp")]
7978 {
7979 for name in mcp_tool_dispatch.keys() {
7980 declared.push(name.clone());
7981 }
7982 }
7983
7984 let mut results: Vec<(String, String)> = Vec::new();
7986 for tc in &tool_calls {
7987 if !declared.iter().any(|d| d == &tc.name) {
7988 results.push((
7989 tc.name.clone(),
7990 format!("Error: '{}' not in declared tools", tc.name),
7991 ));
7992 continue;
7993 }
7994
7995 let result_str;
7997 #[cfg(feature = "mcp")]
7998 {
7999 if let Some(&client_idx) = mcp_tool_dispatch.get(tc.name.as_str()) {
8000 let mcp_result = mcp_clients[client_idx]
8001 .call_tool(&tc.name, tc.input.clone())
8002 .map_err(|e| runtime_err(format!("MCP tool error: {e}")))?;
8003 result_str = mcp_result
8004 .content
8005 .iter()
8006 .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
8007 .collect::<Vec<_>>()
8008 .join("\n");
8009 } else {
8010 result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8011 }
8012 }
8013 #[cfg(not(feature = "mcp"))]
8014 {
8015 result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8016 }
8017
8018 let hook_name = format!("__agent_{}_on_tool_call__", agent_def.name);
8020 if let Some(hook) = self.globals.get(&hook_name).cloned() {
8021 let hook_args = vec![
8022 VmValue::String(Arc::from(tc.name.as_str())),
8023 self.json_value_to_vm(&tc.input),
8024 VmValue::String(Arc::from(result_str.as_str())),
8025 ];
8026 let _ = self.call_value(hook, &hook_args);
8027 }
8028
8029 results.push((tc.name.clone(), result_str));
8030 }
8031
8032 let result_msgs = format_tool_result_messages(provider, &tool_calls, &results);
8034 messages.extend(result_msgs);
8035 }
8036 }
8037 }
8038
8039 Err(runtime_err(format!(
8040 "Agent '{}' exceeded max_turns ({})",
8041 agent_def.name, agent_def.max_turns
8042 )))
8043 }
8044
8045 #[cfg(feature = "native")]
8046 fn execute_tool_call(
8047 &mut self,
8048 tool_name: &str,
8049 input: &serde_json::Value,
8050 ) -> Result<String, TlError> {
8051 let func = self
8053 .globals
8054 .get(tool_name)
8055 .ok_or_else(|| runtime_err(format!("Agent tool function '{tool_name}' not found")))?
8056 .clone();
8057
8058 let args = self.json_to_vm_args(input);
8060
8061 let result = self.call_value(func, &args)?;
8063
8064 Ok(format!("{result}"))
8066 }
8067
8068 #[cfg(feature = "native")]
8069 fn json_to_vm_args(&self, input: &serde_json::Value) -> Vec<VmValue> {
8070 match input {
8071 serde_json::Value::Object(map) => {
8072 map.values().map(|v| self.json_value_to_vm(v)).collect()
8074 }
8075 serde_json::Value::Array(arr) => arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8076 _ => vec![self.json_value_to_vm(input)],
8077 }
8078 }
8079
8080 #[cfg(feature = "native")]
8081 fn json_value_to_vm(&self, val: &serde_json::Value) -> VmValue {
8082 match val {
8083 serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
8084 serde_json::Value::Number(n) => {
8085 if let Some(i) = n.as_i64() {
8086 VmValue::Int(i)
8087 } else if let Some(f) = n.as_f64() {
8088 VmValue::Float(f)
8089 } else {
8090 VmValue::None
8091 }
8092 }
8093 serde_json::Value::Bool(b) => VmValue::Bool(*b),
8094 serde_json::Value::Null => VmValue::None,
8095 serde_json::Value::Array(arr) => VmValue::List(Box::new(
8096 arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8097 )),
8098 serde_json::Value::Object(map) => {
8099 let pairs: Vec<(Arc<str>, VmValue)> = map
8100 .iter()
8101 .map(|(k, v)| (Arc::from(k.as_str()), self.json_value_to_vm(v)))
8102 .collect();
8103 VmValue::Map(Box::new(pairs))
8104 }
8105 }
8106 }
8107
8108 #[cfg(feature = "native")]
8109 fn call_value(&mut self, func: VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
8110 match &func {
8111 VmValue::Function(_) => {
8112 let save_len = self.stack.len();
8114 let func_slot = save_len;
8115 let _args_start = func_slot + 1;
8116 self.stack.push(func.clone());
8117 for arg in args {
8118 self.stack.push(arg.clone());
8119 }
8120 self.ensure_stack(self.stack.len() + 256);
8121
8122 self.do_call(func, func_slot, 0, 1, args.len() as u8)?;
8123
8124 let entry_depth = self.frames.len() - 1;
8126 while self.frames.len() > entry_depth {
8127 if self.run_step(entry_depth)?.is_some() {
8128 break;
8129 }
8130 }
8131
8132 let result = self.stack[func_slot].clone();
8134 self.stack.truncate(save_len);
8135 Ok(result)
8136 }
8137 VmValue::Builtin(id) => {
8138 let id_u16 = *id as u16;
8139 let save_len = self.stack.len();
8140 for arg in args {
8141 self.stack.push(arg.clone());
8142 }
8143 let result = self.call_builtin(id_u16, save_len, args.len())?;
8144 self.stack.truncate(save_len);
8145 Ok(result)
8146 }
8147 _ => Err(runtime_err(format!(
8148 "Agent tool '{}' is not callable",
8149 func.type_name()
8150 ))),
8151 }
8152 }
8153
8154 #[cfg(feature = "native")]
8155 fn parse_window_type(s: &str) -> Option<tl_stream::window::WindowType> {
8156 if let Some(dur) = s.strip_prefix("tumbling:") {
8157 let ms = tl_stream::parse_duration(dur).ok()?;
8158 Some(tl_stream::window::WindowType::Tumbling { duration_ms: ms })
8159 } else if let Some(rest) = s.strip_prefix("sliding:") {
8160 let parts: Vec<&str> = rest.splitn(2, ':').collect();
8161 if parts.len() == 2 {
8162 let wms = tl_stream::parse_duration(parts[0]).ok()?;
8163 let sms = tl_stream::parse_duration(parts[1]).ok()?;
8164 Some(tl_stream::window::WindowType::Sliding {
8165 window_ms: wms,
8166 slide_ms: sms,
8167 })
8168 } else {
8169 None
8170 }
8171 } else if let Some(dur) = s.strip_prefix("session:") {
8172 let ms = tl_stream::parse_duration(dur).ok()?;
8173 Some(tl_stream::window::WindowType::Session { gap_ms: ms })
8174 } else {
8175 None
8176 }
8177 }
8178
8179 #[cfg(feature = "native")]
8180 fn handle_connector_decl(
8181 &mut self,
8182 frame_idx: usize,
8183 type_const: u8,
8184 config_const: u8,
8185 ) -> Result<VmValue, TlError> {
8186 let frame = &self.frames[frame_idx];
8187 let connector_type = match &frame.prototype.constants[type_const as usize] {
8188 Constant::String(s) => s.to_string(),
8189 _ => return Err(runtime_err("Expected string constant for connector type")),
8190 };
8191
8192 let config_args = match &frame.prototype.constants[config_const as usize] {
8193 Constant::AstExprList(args) => args.clone(),
8194 _ => return Err(runtime_err("Expected AST expr list for connector config")),
8195 };
8196
8197 let mut properties = std::collections::HashMap::new();
8198 for arg in &config_args {
8199 if let AstExpr::NamedArg { name: key, value } = arg {
8200 let val_str = match value.as_ref() {
8201 AstExpr::String(s) => s.clone(),
8202 AstExpr::Int(n) => n.to_string(),
8203 AstExpr::Float(f) => f.to_string(),
8204 AstExpr::Bool(b) => b.to_string(),
8205 other => {
8206 if let AstExpr::Ident(ident) = other {
8208 if let Some(val) = self.globals.get(ident.as_str()) {
8209 format!("{val}")
8210 } else {
8211 ident.clone()
8212 }
8213 } else {
8214 format!("{other:?}")
8215 }
8216 }
8217 };
8218 properties.insert(key.clone(), val_str);
8219 }
8220 }
8221
8222 let config = tl_stream::ConnectorConfig {
8223 name: String::new(), connector_type,
8225 properties,
8226 };
8227
8228 Ok(VmValue::Connector(Arc::new(config)))
8229 }
8230
8231 fn generator_next(&mut self, gen_arc: &Arc<Mutex<VmGenerator>>) -> Result<VmValue, TlError> {
8233 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8234 if gn.done {
8235 return Ok(VmValue::None);
8236 }
8237 match &mut gn.kind {
8238 GeneratorKind::UserDefined {
8239 prototype,
8240 upvalues,
8241 saved_stack,
8242 ip,
8243 } => {
8244 let proto = prototype.clone();
8245 let uvs = upvalues.clone();
8246 let stack_snapshot = saved_stack.clone();
8247 let saved_ip = *ip;
8248 drop(gn); let new_base = self.stack.len();
8252 let num_regs = proto.num_registers as usize;
8253 self.ensure_stack(new_base + num_regs + 1);
8254 for (i, val) in stack_snapshot.iter().enumerate() {
8256 self.stack[new_base + i] = val.clone();
8257 }
8258
8259 self.frames.push(CallFrame {
8260 prototype: proto,
8261 ip: saved_ip,
8262 base: new_base,
8263 upvalues: uvs,
8264 });
8265
8266 self.yielded_value = None;
8267 let _result = self.run()?;
8268
8269 if let Some(yielded) = self.yielded_value.take() {
8270 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8272 if let GeneratorKind::UserDefined {
8273 saved_stack, ip, ..
8274 } = &mut gn.kind
8275 {
8276 let num_regs_save = saved_stack.len();
8278 for (i, slot) in saved_stack.iter_mut().enumerate().take(num_regs_save) {
8279 if new_base + i < self.stack.len() {
8280 *slot = self.stack[new_base + i].clone();
8281 }
8282 }
8283 *ip = self.yielded_ip;
8285 }
8286 self.stack.truncate(new_base);
8287 Ok(yielded)
8288 } else {
8289 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8291 gn.done = true;
8292 self.stack.truncate(new_base);
8293 Ok(VmValue::None)
8294 }
8295 }
8296 GeneratorKind::ListIter { items, index } => {
8297 if *index < items.len() {
8298 let val = items[*index].clone();
8299 *index += 1;
8300 Ok(val)
8301 } else {
8302 gn.done = true;
8303 Ok(VmValue::None)
8304 }
8305 }
8306 GeneratorKind::Take { source, remaining } => {
8307 if *remaining == 0 {
8308 gn.done = true;
8309 return Ok(VmValue::None);
8310 }
8311 *remaining -= 1;
8312 let src = source.clone();
8313 drop(gn);
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 }
8319 Ok(val)
8320 }
8321 GeneratorKind::Skip { source, remaining } => {
8322 let src = source.clone();
8323 let skip_n = *remaining;
8324 *remaining = 0;
8325 drop(gn);
8326 for _ in 0..skip_n {
8328 let val = self.generator_next(&src)?;
8329 if matches!(val, VmValue::None) {
8330 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8331 gn.done = true;
8332 return Ok(VmValue::None);
8333 }
8334 }
8335 let val = self.generator_next(&src)?;
8336 if matches!(val, VmValue::None) {
8337 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8338 gn.done = true;
8339 }
8340 Ok(val)
8341 }
8342 GeneratorKind::Map { source, func } => {
8343 let src = source.clone();
8344 let f = func.clone();
8345 drop(gn);
8346 let val = self.generator_next(&src)?;
8347 if matches!(val, VmValue::None) {
8348 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8349 gn.done = true;
8350 return Ok(VmValue::None);
8351 }
8352 self.call_vm_function(&f, &[val])
8353 }
8354 GeneratorKind::Filter { source, func } => {
8355 let src = source.clone();
8356 let f = func.clone();
8357 drop(gn);
8358 loop {
8359 let val = self.generator_next(&src)?;
8360 if matches!(val, VmValue::None) {
8361 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8362 gn.done = true;
8363 return Ok(VmValue::None);
8364 }
8365 let test = self.call_vm_function(&f, std::slice::from_ref(&val))?;
8366 if test.is_truthy() {
8367 return Ok(val);
8368 }
8369 }
8370 }
8371 GeneratorKind::Chain {
8372 first,
8373 second,
8374 on_second,
8375 } => {
8376 if !*on_second {
8377 let src = first.clone();
8378 drop(gn);
8379 let val = self.generator_next(&src)?;
8380 if matches!(val, VmValue::None) {
8381 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8382 if let GeneratorKind::Chain {
8383 on_second, second, ..
8384 } = &mut gn.kind
8385 {
8386 *on_second = true;
8387 let src2 = second.clone();
8388 drop(gn);
8389 return self.generator_next(&src2);
8390 }
8391 }
8392 Ok(val)
8393 } else {
8394 let src = second.clone();
8395 drop(gn);
8396 let val = self.generator_next(&src)?;
8397 if matches!(val, VmValue::None) {
8398 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8399 gn.done = true;
8400 }
8401 Ok(val)
8402 }
8403 }
8404 GeneratorKind::Zip { first, second } => {
8405 let src1 = first.clone();
8406 let src2 = second.clone();
8407 drop(gn);
8408 let val1 = self.generator_next(&src1)?;
8409 let val2 = self.generator_next(&src2)?;
8410 if matches!(val1, VmValue::None) || matches!(val2, VmValue::None) {
8411 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8412 gn.done = true;
8413 return Ok(VmValue::None);
8414 }
8415 Ok(VmValue::List(Box::new(vec![val1, val2])))
8416 }
8417 GeneratorKind::Enumerate { source, index } => {
8418 let src = source.clone();
8419 let idx = *index;
8420 *index += 1;
8421 drop(gn);
8422 let val = self.generator_next(&src)?;
8423 if matches!(val, VmValue::None) {
8424 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8425 gn.done = true;
8426 return Ok(VmValue::None);
8427 }
8428 Ok(VmValue::List(Box::new(vec![VmValue::Int(idx as i64), val])))
8429 }
8430 }
8431 }
8432
8433 #[cfg(feature = "native")]
8435 fn process_schema_global(&mut self, s: &str) {
8436 let rest = &s["__schema__:".len()..];
8438 let parts: Vec<&str> = rest.splitn(3, ':').collect();
8439 if parts.len() < 2 {
8440 return;
8441 }
8442
8443 let schema_name = parts[0];
8444 let mut version: i64 = 0;
8445 let fields_str;
8446
8447 if parts.len() == 3 && parts[1].starts_with('v') {
8448 version = parts[1][1..].parse().unwrap_or(0);
8450 fields_str = parts[2];
8451 } else if parts.len() == 3 {
8452 fields_str = &rest[schema_name.len() + 1..];
8454 } else {
8455 fields_str = parts[1];
8456 }
8457
8458 if version == 0 {
8459 return;
8460 } let mut arrow_fields = Vec::new();
8463 for field_pair in fields_str.split(',') {
8464 let kv: Vec<&str> = field_pair.splitn(2, ':').collect();
8465 if kv.len() == 2 {
8466 let fname = kv[0].trim();
8467 let ftype = kv[1].trim();
8468 let type_name = if ftype.starts_with("Simple(\"") && ftype.ends_with("\")") {
8470 &ftype[8..ftype.len() - 2]
8471 } else {
8472 ftype
8473 };
8474 let dt = crate::schema::type_name_to_arrow_pub(type_name);
8475 arrow_fields.push(tl_data::ArrowField::new(fname, dt, true));
8476 }
8477 }
8478
8479 if !arrow_fields.is_empty() {
8480 let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(arrow_fields));
8481 let _ = self.schema_registry.register(
8482 schema_name,
8483 version,
8484 schema,
8485 crate::schema::SchemaMetadata::default(),
8486 );
8487 }
8488 }
8489
8490 #[cfg(feature = "native")]
8492 fn process_migrate_global(&mut self, s: &str) {
8493 let rest = &s["__migrate__:".len()..];
8495 let parts: Vec<&str> = rest.splitn(4, ':').collect();
8496 if parts.len() < 4 {
8497 return;
8498 }
8499
8500 let schema_name = parts[0];
8501 let from_ver: i64 = parts[1].parse().unwrap_or(0);
8502 let to_ver: i64 = parts[2].parse().unwrap_or(0);
8503 let ops_str = parts[3];
8504
8505 let mut ops = Vec::new();
8506 for op_str in ops_str.split(';') {
8507 let op_parts: Vec<&str> = op_str.splitn(4, ':').collect();
8508 if op_parts.is_empty() {
8509 continue;
8510 }
8511 match op_parts[0] {
8512 "add" if op_parts.len() >= 3 => {
8513 let name = op_parts[1].to_string();
8514 let type_raw = op_parts[2];
8516 let type_name =
8517 if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8518 type_raw[8..type_raw.len() - 2].to_string()
8519 } else {
8520 type_raw.to_string()
8521 };
8522 let default = if op_parts.len() >= 4 && op_parts[3].starts_with("default:") {
8523 Some(
8524 op_parts[3]["default:".len()..]
8525 .trim_matches('"')
8526 .to_string(),
8527 )
8528 } else {
8529 None
8530 };
8531 ops.push(crate::schema::MigrationOp::AddColumn {
8532 name,
8533 type_name,
8534 default,
8535 });
8536 }
8537 "drop" if op_parts.len() >= 2 => {
8538 ops.push(crate::schema::MigrationOp::DropColumn {
8539 name: op_parts[1].to_string(),
8540 });
8541 }
8542 "rename" if op_parts.len() >= 3 => {
8543 ops.push(crate::schema::MigrationOp::RenameColumn {
8544 from: op_parts[1].to_string(),
8545 to: op_parts[2].to_string(),
8546 });
8547 }
8548 "alter" if op_parts.len() >= 3 => {
8549 let type_raw = op_parts[2];
8550 let type_name =
8551 if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8552 type_raw[8..type_raw.len() - 2].to_string()
8553 } else {
8554 type_raw.to_string()
8555 };
8556 ops.push(crate::schema::MigrationOp::AlterType {
8557 column: op_parts[1].to_string(),
8558 new_type: type_name,
8559 });
8560 }
8561 _ => {}
8562 }
8563 }
8564
8565 let _ = self
8566 .schema_registry
8567 .apply_migration(schema_name, from_ver, to_ver, &ops);
8568 }
8569
8570 fn deep_clone_value(&self, val: &VmValue) -> Result<VmValue, TlError> {
8573 match val {
8574 VmValue::List(items) => {
8575 let cloned: Result<Vec<_>, _> =
8576 items.iter().map(|v| self.deep_clone_value(v)).collect();
8577 Ok(VmValue::List(Box::new(cloned?)))
8578 }
8579 VmValue::Map(pairs) => {
8580 let cloned: Result<Vec<_>, _> = pairs
8581 .iter()
8582 .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8583 .collect();
8584 Ok(VmValue::Map(Box::new(cloned?)))
8585 }
8586 VmValue::Set(items) => {
8587 let cloned: Result<Vec<_>, _> =
8588 items.iter().map(|v| self.deep_clone_value(v)).collect();
8589 Ok(VmValue::Set(Box::new(cloned?)))
8590 }
8591 VmValue::StructInstance(inst) => {
8592 let cloned_fields: Result<Vec<_>, _> = inst
8593 .fields
8594 .iter()
8595 .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8596 .collect();
8597 Ok(VmValue::StructInstance(Arc::new(VmStructInstance {
8598 type_name: inst.type_name.clone(),
8599 fields: cloned_fields?,
8600 })))
8601 }
8602 VmValue::EnumInstance(e) => {
8603 let cloned_fields: Result<Vec<_>, _> =
8604 e.fields.iter().map(|v| self.deep_clone_value(v)).collect();
8605 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
8606 type_name: e.type_name.clone(),
8607 variant: e.variant.clone(),
8608 fields: cloned_fields?,
8609 })))
8610 }
8611 #[cfg(feature = "gpu")]
8612 VmValue::GpuTensor(gt) => {
8613 let cloned = tl_gpu::GpuTensor::clone(gt.as_ref());
8614 Ok(VmValue::GpuTensor(Arc::new(cloned)))
8615 }
8616 VmValue::Ref(inner) => self.deep_clone_value(inner),
8617 VmValue::Moved => Err(runtime_err("Cannot clone a moved value".to_string())),
8618 VmValue::Task(_) => Err(runtime_err("Cannot clone a task".to_string())),
8619 VmValue::Channel(_) => Err(runtime_err("Cannot clone a channel".to_string())),
8620 VmValue::Generator(_) => Err(runtime_err("Cannot clone a generator".to_string())),
8621 other => Ok(other.clone()),
8622 }
8623 }
8624
8625 pub fn dispatch_method(
8626 &mut self,
8627 obj: VmValue,
8628 method: &str,
8629 args: &[VmValue],
8630 ) -> Result<VmValue, TlError> {
8631 if method == "clone" {
8633 return self.deep_clone_value(&obj);
8634 }
8635 let obj = match obj {
8637 VmValue::Ref(inner) => inner.as_ref().clone(),
8638 other => other,
8639 };
8640 match &obj {
8641 VmValue::String(s) => self.dispatch_string_method(s.clone(), method, args),
8642 VmValue::List(items) => self.dispatch_list_method((**items).clone(), method, args),
8643 VmValue::Map(pairs) => self.dispatch_map_method((**pairs).clone(), method, args),
8644 VmValue::Set(items) => self.dispatch_set_method((**items).clone(), method, args),
8645 VmValue::Module(m) => {
8646 if let Some(func) = m.exports.get(method).cloned() {
8647 self.call_vm_function(&func, args)
8648 } else {
8649 Err(runtime_err(format!(
8650 "Module '{}' has no export '{}'",
8651 m.name, method
8652 )))
8653 }
8654 }
8655 VmValue::StructInstance(inst) => {
8656 let mangled = format!("{}::{}", inst.type_name, method);
8658 if let Some(func) = self.globals.get(&mangled).cloned() {
8659 let mut all_args = vec![obj.clone()];
8660 all_args.extend_from_slice(args);
8661 self.call_vm_function(&func, &all_args)
8662 } else {
8663 Err(runtime_err(format!(
8664 "No method '{}' on struct '{}'",
8665 method, inst.type_name
8666 )))
8667 }
8668 }
8669 #[cfg(feature = "python")]
8670 VmValue::PyObject(wrapper) => crate::python::py_call_method(wrapper, method, args),
8671 #[cfg(feature = "gpu")]
8672 VmValue::GpuTensor(gt) => match method {
8673 "to_cpu" => {
8674 let cpu = gt.to_cpu().map_err(runtime_err)?;
8675 Ok(VmValue::Tensor(Arc::new(cpu)))
8676 }
8677 "shape" => {
8678 let shape_list = Box::new(
8679 gt.shape
8680 .iter()
8681 .map(|&d| VmValue::Int(d as i64))
8682 .collect::<Vec<_>>(),
8683 );
8684 Ok(VmValue::List(shape_list))
8685 }
8686 "dtype" => Ok(VmValue::String(Arc::from(format!("{}", gt.dtype).as_str()))),
8687 _ => Err(runtime_err(format!("No method '{}' on gpu_tensor", method))),
8688 },
8689 _ => {
8690 let type_name = obj.type_name();
8692 let mangled = format!("{}::{}", type_name, method);
8693 if let Some(func) = self.globals.get(&mangled).cloned() {
8694 let mut all_args = vec![obj];
8695 all_args.extend_from_slice(args);
8696 self.call_vm_function(&func, &all_args)
8697 } else {
8698 Err(runtime_err(format!(
8699 "No method '{}' on type '{}'",
8700 method, type_name
8701 )))
8702 }
8703 }
8704 }
8705 }
8706
8707 fn dispatch_string_method(
8709 &self,
8710 s: Arc<str>,
8711 method: &str,
8712 args: &[VmValue],
8713 ) -> Result<VmValue, TlError> {
8714 match method {
8715 "len" => Ok(VmValue::Int(s.len() as i64)),
8716 "split" => {
8717 let sep = match args.first() {
8718 Some(VmValue::String(sep)) => sep.to_string(),
8719 _ => return Err(runtime_err("split() expects a string separator")),
8720 };
8721 let parts: Vec<VmValue> = s
8722 .split(&sep)
8723 .map(|p| VmValue::String(Arc::from(p)))
8724 .collect();
8725 Ok(VmValue::List(Box::new(parts)))
8726 }
8727 "trim" => Ok(VmValue::String(Arc::from(s.trim()))),
8728 "contains" => {
8729 let needle = match args.first() {
8730 Some(VmValue::String(n)) => n.to_string(),
8731 _ => return Err(runtime_err("contains() expects a string")),
8732 };
8733 Ok(VmValue::Bool(s.contains(&needle)))
8734 }
8735 "replace" => {
8736 if args.len() < 2 {
8737 return Err(runtime_err("replace() expects 2 arguments (old, new)"));
8738 }
8739 let old = match &args[0] {
8740 VmValue::String(s) => s.to_string(),
8741 _ => return Err(runtime_err("replace() arg must be string")),
8742 };
8743 let new = match &args[1] {
8744 VmValue::String(s) => s.to_string(),
8745 _ => return Err(runtime_err("replace() arg must be string")),
8746 };
8747 Ok(VmValue::String(Arc::from(s.replace(&old, &new).as_str())))
8748 }
8749 "starts_with" => {
8750 let prefix = match args.first() {
8751 Some(VmValue::String(p)) => p.to_string(),
8752 _ => return Err(runtime_err("starts_with() expects a string")),
8753 };
8754 Ok(VmValue::Bool(s.starts_with(&prefix)))
8755 }
8756 "ends_with" => {
8757 let suffix = match args.first() {
8758 Some(VmValue::String(p)) => p.to_string(),
8759 _ => return Err(runtime_err("ends_with() expects a string")),
8760 };
8761 Ok(VmValue::Bool(s.ends_with(&suffix)))
8762 }
8763 "to_upper" => Ok(VmValue::String(Arc::from(s.to_uppercase().as_str()))),
8764 "to_lower" => Ok(VmValue::String(Arc::from(s.to_lowercase().as_str()))),
8765 "chars" => {
8766 let chars: Vec<VmValue> = s
8767 .chars()
8768 .map(|c| VmValue::String(Arc::from(c.to_string().as_str())))
8769 .collect();
8770 Ok(VmValue::List(Box::new(chars)))
8771 }
8772 "repeat" => {
8773 let n = match args.first() {
8774 Some(VmValue::Int(n)) => *n as usize,
8775 _ => return Err(runtime_err("repeat() expects an integer")),
8776 };
8777 Ok(VmValue::String(Arc::from(s.repeat(n).as_str())))
8778 }
8779 "index_of" => {
8780 let needle = match args.first() {
8781 Some(VmValue::String(n)) => n.to_string(),
8782 _ => return Err(runtime_err("index_of() expects a string")),
8783 };
8784 Ok(VmValue::Int(
8785 s.find(&needle).map(|i| i as i64).unwrap_or(-1),
8786 ))
8787 }
8788 "substring" => {
8789 if args.len() < 2 {
8790 return Err(runtime_err("substring() expects start and end"));
8791 }
8792 let start = match &args[0] {
8793 VmValue::Int(n) => *n as usize,
8794 _ => return Err(runtime_err("substring() expects integers")),
8795 };
8796 let end = match &args[1] {
8797 VmValue::Int(n) => *n as usize,
8798 _ => return Err(runtime_err("substring() expects integers")),
8799 };
8800 let end = end.min(s.len());
8801 let start = start.min(end);
8802 Ok(VmValue::String(Arc::from(&s[start..end])))
8803 }
8804 "pad_left" => {
8805 if args.is_empty() {
8806 return Err(runtime_err("pad_left() expects width"));
8807 }
8808 let width = match &args[0] {
8809 VmValue::Int(n) => *n as usize,
8810 _ => return Err(runtime_err("pad_left() expects integer width")),
8811 };
8812 let ch = match args.get(1) {
8813 Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8814 _ => ' ',
8815 };
8816 if s.len() >= width {
8817 Ok(VmValue::String(s))
8818 } else {
8819 Ok(VmValue::String(Arc::from(
8820 format!(
8821 "{}{}",
8822 std::iter::repeat_n(ch, width - s.len()).collect::<String>(),
8823 s
8824 )
8825 .as_str(),
8826 )))
8827 }
8828 }
8829 "pad_right" => {
8830 if args.is_empty() {
8831 return Err(runtime_err("pad_right() expects width"));
8832 }
8833 let width = match &args[0] {
8834 VmValue::Int(n) => *n as usize,
8835 _ => return Err(runtime_err("pad_right() expects integer width")),
8836 };
8837 let ch = match args.get(1) {
8838 Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8839 _ => ' ',
8840 };
8841 if s.len() >= width {
8842 Ok(VmValue::String(s))
8843 } else {
8844 Ok(VmValue::String(Arc::from(
8845 format!(
8846 "{}{}",
8847 s,
8848 std::iter::repeat_n(ch, width - s.len()).collect::<String>()
8849 )
8850 .as_str(),
8851 )))
8852 }
8853 }
8854 "join" => {
8855 let items = match args.first() {
8857 Some(VmValue::List(items)) => items,
8858 _ => return Err(runtime_err("join() expects a list")),
8859 };
8860 let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
8861 Ok(VmValue::String(Arc::from(parts.join(s.as_ref()).as_str())))
8862 }
8863 "trim_start" => Ok(VmValue::String(Arc::from(s.trim_start()))),
8864 "trim_end" => Ok(VmValue::String(Arc::from(s.trim_end()))),
8865 "count" => {
8866 if args.is_empty() {
8867 return Err(runtime_err("count() expects a substring"));
8868 }
8869 if let VmValue::String(sub) = &args[0] {
8870 Ok(VmValue::Int(s.matches(sub.as_ref()).count() as i64))
8871 } else {
8872 Err(runtime_err("count() expects a string"))
8873 }
8874 }
8875 "is_empty" => Ok(VmValue::Bool(s.is_empty())),
8876 "is_numeric" => Ok(VmValue::Bool(
8877 s.chars()
8878 .all(|c| c.is_ascii_digit() || c == '.' || c == '-'),
8879 )),
8880 "is_alpha" => Ok(VmValue::Bool(
8881 !s.is_empty() && s.chars().all(|c| c.is_alphabetic()),
8882 )),
8883 "strip_prefix" => {
8884 if args.is_empty() {
8885 return Err(runtime_err("strip_prefix() expects a string"));
8886 }
8887 if let VmValue::String(prefix) = &args[0] {
8888 match s.strip_prefix(prefix.as_ref()) {
8889 Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8890 None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8891 }
8892 } else {
8893 Err(runtime_err("strip_prefix() expects a string"))
8894 }
8895 }
8896 "strip_suffix" => {
8897 if args.is_empty() {
8898 return Err(runtime_err("strip_suffix() expects a string"));
8899 }
8900 if let VmValue::String(suffix) = &args[0] {
8901 match s.strip_suffix(suffix.as_ref()) {
8902 Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8903 None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8904 }
8905 } else {
8906 Err(runtime_err("strip_suffix() expects a string"))
8907 }
8908 }
8909 _ => Err(runtime_err(format!("No method '{}' on string", method))),
8910 }
8911 }
8912
8913 fn dispatch_list_method(
8915 &mut self,
8916 items: Vec<VmValue>,
8917 method: &str,
8918 args: &[VmValue],
8919 ) -> Result<VmValue, TlError> {
8920 match method {
8921 "len" => Ok(VmValue::Int(items.len() as i64)),
8922 "push" => {
8923 if args.is_empty() {
8924 return Err(runtime_err("push() expects 1 argument"));
8925 }
8926 let mut new_items = items;
8927 new_items.push(args[0].clone());
8928 Ok(VmValue::List(Box::new(new_items)))
8929 }
8930 "map" => {
8931 if args.is_empty() {
8932 return Err(runtime_err("map() expects a function"));
8933 }
8934 let func = &args[0];
8935 let mut result = Vec::new();
8936 for item in items {
8937 let val = self.call_vm_function(func, &[item])?;
8938 result.push(val);
8939 }
8940 Ok(VmValue::List(Box::new(result)))
8941 }
8942 "filter" => {
8943 if args.is_empty() {
8944 return Err(runtime_err("filter() expects a function"));
8945 }
8946 let func = &args[0];
8947 let mut result = Vec::new();
8948 for item in items {
8949 let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
8950 if val.is_truthy() {
8951 result.push(item);
8952 }
8953 }
8954 Ok(VmValue::List(Box::new(result)))
8955 }
8956 "reduce" => {
8957 if args.len() < 2 {
8958 return Err(runtime_err("reduce() expects initial value and function"));
8959 }
8960 let mut acc = args[0].clone();
8961 let func = &args[1];
8962 for item in items {
8963 acc = self.call_vm_function(func, &[acc, item])?;
8964 }
8965 Ok(acc)
8966 }
8967 "sort" => {
8968 let mut sorted = items;
8969 sorted.sort_by(|a, b| match (a, b) {
8970 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
8971 (VmValue::Float(x), VmValue::Float(y)) => {
8972 x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
8973 }
8974 (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
8975 _ => std::cmp::Ordering::Equal,
8976 });
8977 Ok(VmValue::List(Box::new(sorted)))
8978 }
8979 "reverse" => {
8980 let mut reversed = items;
8981 reversed.reverse();
8982 Ok(VmValue::List(Box::new(reversed)))
8983 }
8984 "contains" => {
8985 if args.is_empty() {
8986 return Err(runtime_err("contains() expects a value"));
8987 }
8988 let needle = &args[0];
8989 let found = items.iter().any(|item| match (item, needle) {
8990 (VmValue::Int(a), VmValue::Int(b)) => a == b,
8991 (VmValue::Float(a), VmValue::Float(b)) => a == b,
8992 (VmValue::String(a), VmValue::String(b)) => a == b,
8993 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
8994 (VmValue::None, VmValue::None) => true,
8995 _ => false,
8996 });
8997 Ok(VmValue::Bool(found))
8998 }
8999 "index_of" => {
9000 if args.is_empty() {
9001 return Err(runtime_err("index_of() expects a value"));
9002 }
9003 let needle = &args[0];
9004 let idx = items.iter().position(|item| match (item, needle) {
9005 (VmValue::Int(a), VmValue::Int(b)) => a == b,
9006 (VmValue::Float(a), VmValue::Float(b)) => a == b,
9007 (VmValue::String(a), VmValue::String(b)) => a == b,
9008 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9009 (VmValue::None, VmValue::None) => true,
9010 _ => false,
9011 });
9012 Ok(VmValue::Int(idx.map(|i| i as i64).unwrap_or(-1)))
9013 }
9014 "slice" => {
9015 if args.len() < 2 {
9016 return Err(runtime_err("slice() expects start and end"));
9017 }
9018 let start = match &args[0] {
9019 VmValue::Int(n) => *n as usize,
9020 _ => return Err(runtime_err("slice() expects integers")),
9021 };
9022 let end = match &args[1] {
9023 VmValue::Int(n) => *n as usize,
9024 _ => return Err(runtime_err("slice() expects integers")),
9025 };
9026 let end = end.min(items.len());
9027 let start = start.min(end);
9028 Ok(VmValue::List(Box::new(items[start..end].to_vec())))
9029 }
9030 "flat_map" => {
9031 if args.is_empty() {
9032 return Err(runtime_err("flat_map() expects a function"));
9033 }
9034 let func = &args[0];
9035 let mut result = Vec::new();
9036 for item in items {
9037 let val = self.call_vm_function(func, &[item])?;
9038 match val {
9039 VmValue::List(sub) => result.extend(*sub),
9040 other => result.push(other),
9041 }
9042 }
9043 Ok(VmValue::List(Box::new(result)))
9044 }
9045 "find" => {
9046 if args.is_empty() {
9047 return Err(runtime_err("find() expects a predicate function"));
9048 }
9049 let func = &args[0];
9050 for item in items {
9051 let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9052 if val.is_truthy() {
9053 return Ok(item);
9054 }
9055 }
9056 Ok(VmValue::None)
9057 }
9058 "sort_by" => {
9059 if args.is_empty() {
9060 return Err(runtime_err("sort_by() expects a key function"));
9061 }
9062 let func = &args[0];
9063 let mut keyed: Vec<(VmValue, VmValue)> = Vec::with_capacity(items.len());
9064 for item in items {
9065 let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9066 keyed.push((key, item));
9067 }
9068 keyed.sort_by(|(a, _), (b, _)| match (a, b) {
9069 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9070 (VmValue::Float(x), VmValue::Float(y)) => {
9071 x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9072 }
9073 (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9074 _ => std::cmp::Ordering::Equal,
9075 });
9076 Ok(VmValue::List(Box::new(
9077 keyed.into_iter().map(|(_, v)| v).collect(),
9078 )))
9079 }
9080 "group_by" => {
9081 if args.is_empty() {
9082 return Err(runtime_err("group_by() expects a key function"));
9083 }
9084 let func = &args[0];
9085 let mut groups: Vec<(Arc<str>, Vec<VmValue>)> = Vec::new();
9086 for item in items {
9087 let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9088 let key_str: Arc<str> = match &key {
9089 VmValue::String(s) => s.clone(),
9090 other => Arc::from(format!("{other}").as_str()),
9091 };
9092 if let Some(group) = groups.iter_mut().find(|(k, _)| *k == key_str) {
9093 group.1.push(item);
9094 } else {
9095 groups.push((key_str, vec![item]));
9096 }
9097 }
9098 let map_pairs: Vec<(Arc<str>, VmValue)> = groups
9099 .into_iter()
9100 .map(|(k, v)| (k, VmValue::List(Box::new(v))))
9101 .collect();
9102 Ok(VmValue::Map(Box::new(map_pairs)))
9103 }
9104 "unique" => {
9105 let mut seen = Vec::new();
9106 let mut result = Vec::new();
9107 for item in &items {
9108 let is_dup = seen.iter().any(|s| vm_values_equal(s, item));
9109 if !is_dup {
9110 seen.push(item.clone());
9111 result.push(item.clone());
9112 }
9113 }
9114 Ok(VmValue::List(Box::new(result)))
9115 }
9116 "flatten" => {
9117 let mut result = Vec::new();
9118 for item in items {
9119 match item {
9120 VmValue::List(sub) => result.extend(*sub),
9121 other => result.push(other),
9122 }
9123 }
9124 Ok(VmValue::List(Box::new(result)))
9125 }
9126 "chunk" => {
9127 if args.is_empty() {
9128 return Err(runtime_err("chunk() expects a size"));
9129 }
9130 let n = match &args[0] {
9131 VmValue::Int(n) if *n > 0 => *n as usize,
9132 _ => return Err(runtime_err("chunk() expects a positive integer")),
9133 };
9134 let chunks: Vec<VmValue> = items
9135 .chunks(n)
9136 .map(|c| VmValue::List(Box::new(c.to_vec())))
9137 .collect();
9138 Ok(VmValue::List(Box::new(chunks)))
9139 }
9140 "insert" => {
9141 if args.len() < 2 {
9142 return Err(runtime_err("insert() expects index and value"));
9143 }
9144 let idx = match &args[0] {
9145 VmValue::Int(n) => *n as usize,
9146 _ => return Err(runtime_err("insert() expects integer index")),
9147 };
9148 let mut new_items = items;
9149 if idx > new_items.len() {
9150 return Err(runtime_err("insert() index out of bounds"));
9151 }
9152 new_items.insert(idx, args[1].clone());
9153 Ok(VmValue::List(Box::new(new_items)))
9154 }
9155 "remove_at" => {
9156 if args.is_empty() {
9157 return Err(runtime_err("remove_at() expects an index"));
9158 }
9159 let idx = match &args[0] {
9160 VmValue::Int(n) => *n as usize,
9161 _ => return Err(runtime_err("remove_at() expects integer index")),
9162 };
9163 let mut new_items = items;
9164 if idx >= new_items.len() {
9165 return Err(runtime_err("remove_at() index out of bounds"));
9166 }
9167 let removed = new_items.remove(idx);
9168 Ok(removed)
9169 }
9170 "is_empty" => Ok(VmValue::Bool(items.is_empty())),
9171 "sum" => {
9172 let mut int_sum: i64 = 0;
9173 let mut has_float = false;
9174 let mut float_sum: f64 = 0.0;
9175 for item in &items {
9176 match item {
9177 VmValue::Int(n) => {
9178 if has_float {
9179 float_sum += *n as f64;
9180 } else {
9181 int_sum += n;
9182 }
9183 }
9184 VmValue::Float(f) => {
9185 if !has_float {
9186 has_float = true;
9187 float_sum = int_sum as f64;
9188 }
9189 float_sum += f;
9190 }
9191 _ => return Err(runtime_err("sum() requires numeric list")),
9192 }
9193 }
9194 if has_float {
9195 Ok(VmValue::Float(float_sum))
9196 } else {
9197 Ok(VmValue::Int(int_sum))
9198 }
9199 }
9200 "min" => {
9201 if items.is_empty() {
9202 return Ok(VmValue::None);
9203 }
9204 let mut min_val = items[0].clone();
9205 for item in &items[1..] {
9206 match (&min_val, item) {
9207 (VmValue::Int(a), VmValue::Int(b)) if b < a => min_val = item.clone(),
9208 (VmValue::Float(a), VmValue::Float(b)) if b < a => min_val = item.clone(),
9209 _ => {}
9210 }
9211 }
9212 Ok(min_val)
9213 }
9214 "max" => {
9215 if items.is_empty() {
9216 return Ok(VmValue::None);
9217 }
9218 let mut max_val = items[0].clone();
9219 for item in &items[1..] {
9220 match (&max_val, item) {
9221 (VmValue::Int(a), VmValue::Int(b)) if b > a => max_val = item.clone(),
9222 (VmValue::Float(a), VmValue::Float(b)) if b > a => max_val = item.clone(),
9223 _ => {}
9224 }
9225 }
9226 Ok(max_val)
9227 }
9228 "each" => {
9229 if args.is_empty() {
9230 return Err(runtime_err("each() expects a function"));
9231 }
9232 let func = &args[0];
9233 for item in items {
9234 self.call_vm_function(func, &[item])?;
9235 }
9236 Ok(VmValue::None)
9237 }
9238 "zip" => {
9239 if args.is_empty() {
9240 return Err(runtime_err("zip() expects a list"));
9241 }
9242 let other = match &args[0] {
9243 VmValue::List(other) => other.as_slice(),
9244 _ => return Err(runtime_err("zip() expects a list")),
9245 };
9246 let len = items.len().min(other.len());
9247 let zipped: Vec<VmValue> = items[..len]
9248 .iter()
9249 .zip(other[..len].iter())
9250 .map(|(a, b)| VmValue::List(Box::new(vec![a.clone(), b.clone()])))
9251 .collect();
9252 Ok(VmValue::List(Box::new(zipped)))
9253 }
9254 "join" => {
9255 let sep = match args.first() {
9256 Some(VmValue::String(s)) => s.as_ref(),
9257 _ => "",
9258 };
9259 let s: String = items
9260 .iter()
9261 .map(|v| format!("{v}"))
9262 .collect::<Vec<_>>()
9263 .join(sep);
9264 Ok(VmValue::String(Arc::from(s.as_str())))
9265 }
9266 _ => Err(runtime_err(format!("No method '{}' on list", method))),
9267 }
9268 }
9269
9270 fn dispatch_map_method(
9272 &mut self,
9273 pairs: Vec<(Arc<str>, VmValue)>,
9274 method: &str,
9275 args: &[VmValue],
9276 ) -> Result<VmValue, TlError> {
9277 match method {
9278 "len" => Ok(VmValue::Int(pairs.len() as i64)),
9279 "keys" => Ok(VmValue::List(Box::new(
9280 pairs
9281 .iter()
9282 .map(|(k, _)| VmValue::String(k.clone()))
9283 .collect(),
9284 ))),
9285 "values" => Ok(VmValue::List(Box::new(
9286 pairs.iter().map(|(_, v)| v.clone()).collect(),
9287 ))),
9288 "contains_key" => {
9289 if args.is_empty() {
9290 return Err(runtime_err("contains_key() expects a key"));
9291 }
9292 if let VmValue::String(key) = &args[0] {
9293 Ok(VmValue::Bool(
9294 pairs.iter().any(|(k, _)| k.as_ref() == key.as_ref()),
9295 ))
9296 } else {
9297 Err(runtime_err("contains_key() expects a string key"))
9298 }
9299 }
9300 "remove" => {
9301 if args.is_empty() {
9302 return Err(runtime_err("remove() expects a key"));
9303 }
9304 if let VmValue::String(key) = &args[0] {
9305 let new_pairs: Vec<(Arc<str>, VmValue)> = pairs
9306 .into_iter()
9307 .filter(|(k, _)| k.as_ref() != key.as_ref())
9308 .collect();
9309 Ok(VmValue::Map(Box::new(new_pairs)))
9310 } else {
9311 Err(runtime_err("remove() expects a string key"))
9312 }
9313 }
9314 "get" => {
9315 if args.is_empty() {
9316 return Err(runtime_err("get() expects a key"));
9317 }
9318 if let VmValue::String(key) = &args[0] {
9319 let default = args.get(1).cloned().unwrap_or(VmValue::None);
9320 let found = pairs.iter().find(|(k, _)| k.as_ref() == key.as_ref());
9321 Ok(found.map(|(_, v)| v.clone()).unwrap_or(default))
9322 } else {
9323 Err(runtime_err("get() expects a string key"))
9324 }
9325 }
9326 "merge" => {
9327 if args.is_empty() {
9328 return Err(runtime_err("merge() expects a map"));
9329 }
9330 if let VmValue::Map(other) = &args[0] {
9331 let mut merged = pairs;
9332 for (k, v) in other.iter() {
9333 if let Some(existing) =
9334 merged.iter_mut().find(|(mk, _)| mk.as_ref() == k.as_ref())
9335 {
9336 existing.1 = v.clone();
9337 } else {
9338 merged.push((k.clone(), v.clone()));
9339 }
9340 }
9341 Ok(VmValue::Map(Box::new(merged)))
9342 } else {
9343 Err(runtime_err("merge() expects a map"))
9344 }
9345 }
9346 "entries" => {
9347 let entries: Vec<VmValue> = pairs
9348 .iter()
9349 .map(|(k, v)| {
9350 VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]))
9351 })
9352 .collect();
9353 Ok(VmValue::List(Box::new(entries)))
9354 }
9355 "map_values" => {
9356 if args.is_empty() {
9357 return Err(runtime_err("map_values() expects a function"));
9358 }
9359 let func = &args[0];
9360 let mut result = Vec::new();
9361 for (k, v) in pairs {
9362 let new_v = self.call_vm_function(func, &[v])?;
9363 result.push((k, new_v));
9364 }
9365 Ok(VmValue::Map(Box::new(result)))
9366 }
9367 "filter" => {
9368 if args.is_empty() {
9369 return Err(runtime_err("filter() expects a predicate function"));
9370 }
9371 let func = &args[0];
9372 let mut result = Vec::new();
9373 for (k, v) in pairs {
9374 let val =
9375 self.call_vm_function(func, &[VmValue::String(k.clone()), v.clone()])?;
9376 if val.is_truthy() {
9377 result.push((k, v));
9378 }
9379 }
9380 Ok(VmValue::Map(Box::new(result)))
9381 }
9382 "set" => {
9383 if args.len() < 2 {
9384 return Err(runtime_err("set() expects key and value"));
9385 }
9386 if let VmValue::String(key) = &args[0] {
9387 let mut new_pairs = pairs;
9388 if let Some(existing) = new_pairs
9389 .iter_mut()
9390 .find(|(k, _)| k.as_ref() == key.as_ref())
9391 {
9392 existing.1 = args[1].clone();
9393 } else {
9394 new_pairs.push((key.clone(), args[1].clone()));
9395 }
9396 Ok(VmValue::Map(Box::new(new_pairs)))
9397 } else {
9398 Err(runtime_err("set() expects a string key"))
9399 }
9400 }
9401 "is_empty" => Ok(VmValue::Bool(pairs.is_empty())),
9402 _ => Err(runtime_err(format!("No method '{}' on map", method))),
9403 }
9404 }
9405
9406 fn dispatch_set_method(
9408 &self,
9409 items: Vec<VmValue>,
9410 method: &str,
9411 args: &[VmValue],
9412 ) -> Result<VmValue, TlError> {
9413 match method {
9414 "len" => Ok(VmValue::Int(items.len() as i64)),
9415 "contains" => {
9416 if args.is_empty() {
9417 return Err(runtime_err("contains() expects a value"));
9418 }
9419 Ok(VmValue::Bool(
9420 items.iter().any(|x| vm_values_equal(x, &args[0])),
9421 ))
9422 }
9423 "add" => {
9424 if args.is_empty() {
9425 return Err(runtime_err("add() expects a value"));
9426 }
9427 let mut new_items = items;
9428 if !new_items.iter().any(|x| vm_values_equal(x, &args[0])) {
9429 new_items.push(args[0].clone());
9430 }
9431 Ok(VmValue::Set(Box::new(new_items)))
9432 }
9433 "remove" => {
9434 if args.is_empty() {
9435 return Err(runtime_err("remove() expects a value"));
9436 }
9437 let new_items: Vec<VmValue> = items
9438 .into_iter()
9439 .filter(|x| !vm_values_equal(x, &args[0]))
9440 .collect();
9441 Ok(VmValue::Set(Box::new(new_items)))
9442 }
9443 "to_list" => Ok(VmValue::List(Box::new(items))),
9444 "union" => {
9445 if args.is_empty() {
9446 return Err(runtime_err("union() expects a set"));
9447 }
9448 if let VmValue::Set(b) = &args[0] {
9449 let mut result = items;
9450 for item in b.iter() {
9451 if !result.iter().any(|x| vm_values_equal(x, item)) {
9452 result.push(item.clone());
9453 }
9454 }
9455 Ok(VmValue::Set(Box::new(result)))
9456 } else {
9457 Err(runtime_err("union() expects a set"))
9458 }
9459 }
9460 "intersection" => {
9461 if args.is_empty() {
9462 return Err(runtime_err("intersection() expects a set"));
9463 }
9464 if let VmValue::Set(b) = &args[0] {
9465 let result: Vec<VmValue> = items
9466 .into_iter()
9467 .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
9468 .collect();
9469 Ok(VmValue::Set(Box::new(result)))
9470 } else {
9471 Err(runtime_err("intersection() expects a set"))
9472 }
9473 }
9474 "difference" => {
9475 if args.is_empty() {
9476 return Err(runtime_err("difference() expects a set"));
9477 }
9478 if let VmValue::Set(b) = &args[0] {
9479 let result: Vec<VmValue> = items
9480 .into_iter()
9481 .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
9482 .collect();
9483 Ok(VmValue::Set(Box::new(result)))
9484 } else {
9485 Err(runtime_err("difference() expects a set"))
9486 }
9487 }
9488 _ => Err(runtime_err(format!("No method '{}' on set", method))),
9489 }
9490 }
9491
9492 #[cfg(feature = "native")]
9494 fn handle_import(&mut self, path: &str, alias: &str) -> Result<VmValue, TlError> {
9495 let resolved = if let Some(ref base) = self.file_path {
9497 let base_dir = std::path::Path::new(base)
9498 .parent()
9499 .unwrap_or(std::path::Path::new("."));
9500 let candidate = base_dir.join(path);
9501 if candidate.exists() {
9502 candidate.to_string_lossy().to_string()
9503 } else {
9504 path.to_string()
9505 }
9506 } else {
9507 path.to_string()
9508 };
9509
9510 if self.importing_files.contains(&resolved) {
9512 return Err(runtime_err(format!("Circular import detected: {resolved}")));
9513 }
9514
9515 if let Some(exports) = self.module_cache.get(&resolved) {
9517 let exports = exports.clone();
9518 return self.bind_import_exports(exports, alias);
9519 }
9520
9521 let source = std::fs::read_to_string(&resolved)
9523 .map_err(|e| runtime_err(format!("Cannot import '{}': {}", resolved, e)))?;
9524 let program = tl_parser::parse(&source)
9525 .map_err(|e| runtime_err(format!("Parse error in '{}': {}", resolved, e)))?;
9526 let proto = crate::compiler::compile(&program)
9527 .map_err(|e| runtime_err(format!("Compile error in '{}': {}", resolved, e)))?;
9528
9529 self.importing_files.insert(resolved.clone());
9531
9532 let mut import_vm = Vm::new();
9534 import_vm.file_path = Some(resolved.clone());
9535 import_vm.globals = self.globals.clone();
9536 import_vm.importing_files = self.importing_files.clone();
9537 import_vm.module_cache = self.module_cache.clone();
9538 import_vm.package_roots = self.package_roots.clone();
9539 import_vm.project_root = self.project_root.clone();
9540 import_vm.execute(&proto)?;
9541
9542 self.importing_files.remove(&resolved);
9543
9544 let mut exports = HashMap::new();
9546
9547 for (k, v) in &import_vm.globals {
9549 if !self.globals.contains_key(k) {
9550 exports.insert(k.clone(), v.clone());
9551 }
9552 }
9553
9554 for (name, reg) in &proto.top_level_locals {
9556 if !name.starts_with("__enum_") && !exports.contains_key(name) {
9557 let stack_idx = reg;
9558 if (*stack_idx as usize) < import_vm.stack.len() {
9559 let val = import_vm.stack[*stack_idx as usize].clone();
9560 if !matches!(val, VmValue::None) || name.starts_with("_") {
9561 exports.insert(name.clone(), val);
9562 }
9563 }
9564 }
9565 }
9566
9567 self.module_cache.insert(resolved, exports.clone());
9569 for (k, v) in import_vm.module_cache {
9571 self.module_cache.entry(k).or_insert(v);
9572 }
9573
9574 self.bind_import_exports(exports, alias)
9575 }
9576
9577 #[cfg(feature = "native")]
9579 fn bind_import_exports(
9580 &mut self,
9581 exports: HashMap<String, VmValue>,
9582 alias: &str,
9583 ) -> Result<VmValue, TlError> {
9584 if alias.is_empty() {
9585 for (k, v) in &exports {
9587 self.globals.insert(k.clone(), v.clone());
9588 }
9589 Ok(VmValue::None)
9590 } else {
9591 let module = VmModule {
9593 name: Arc::from(alias),
9594 exports,
9595 };
9596 let module_val = VmValue::Module(Arc::new(module));
9597 self.globals.insert(alias.to_string(), module_val.clone());
9598 Ok(module_val)
9599 }
9600 }
9601
9602 #[cfg(feature = "native")]
9604 fn handle_use_import(
9605 &mut self,
9606 path_str: &str,
9607 extra_a: u8,
9608 kind: u8,
9609 _frame_idx: usize,
9610 ) -> Result<VmValue, TlError> {
9611 match kind {
9612 0 => {
9613 let segments: Vec<&str> = path_str.split('.').collect();
9615 let file_path = self.resolve_use_path(&segments)?;
9616 let _last = segments.last().copied().unwrap_or("");
9618 self.handle_import(&file_path, "")?;
9619 Ok(VmValue::None)
9624 }
9625 1 => {
9626 let brace_start = path_str.find('{').unwrap_or(path_str.len());
9628 let prefix = path_str[..brace_start].trim_end_matches('.');
9629 let segments: Vec<&str> = prefix.split('.').collect();
9630 let file_path = self.resolve_use_path(&segments)?;
9631 self.handle_import(&file_path, "")?;
9632 Ok(VmValue::None)
9633 }
9634 2 => {
9635 let prefix = path_str.trim_end_matches(".*");
9637 let segments: Vec<&str> = prefix.split('.').collect();
9638 let file_path = self.resolve_use_path(&segments)?;
9639 self.handle_import(&file_path, "")?;
9640 Ok(VmValue::None)
9641 }
9642 3 => {
9643 let segments: Vec<&str> = path_str.split('.').collect();
9645 let file_path = self.resolve_use_path(&segments)?;
9646 let alias_str = if let Some(frame) = self.frames.last() {
9649 if let Some(crate::chunk::Constant::String(s)) =
9650 frame.prototype.constants.get(extra_a as usize)
9651 {
9652 s.to_string()
9653 } else {
9654 segments.last().copied().unwrap_or("module").to_string()
9655 }
9656 } else {
9657 segments.last().copied().unwrap_or("module").to_string()
9658 };
9659 self.handle_import(&file_path, &alias_str)?;
9660 Ok(VmValue::None)
9661 }
9662 _ => Err(runtime_err(format!("Unknown use-import kind: {kind}"))),
9663 }
9664 }
9665
9666 #[cfg(feature = "native")]
9668 fn resolve_use_path(&self, segments: &[&str]) -> Result<String, TlError> {
9669 if segments.contains(&"..") {
9671 return Err(runtime_err("Import paths cannot contain '..'"));
9672 }
9673
9674 let base_dir = if let Some(ref fp) = self.file_path {
9675 std::path::Path::new(fp)
9676 .parent()
9677 .unwrap_or(std::path::Path::new("."))
9678 .to_path_buf()
9679 } else {
9680 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
9681 };
9682
9683 let rel_path = segments.join("/");
9684
9685 let file_path = base_dir.join(format!("{rel_path}.tl"));
9687 if file_path.exists() {
9688 return Ok(file_path.to_string_lossy().to_string());
9689 }
9690
9691 let dir_path = base_dir.join(&rel_path).join("mod.tl");
9693 if dir_path.exists() {
9694 return Ok(dir_path.to_string_lossy().to_string());
9695 }
9696
9697 if segments.len() > 1 {
9699 let parent = &segments[..segments.len() - 1];
9700 let parent_path = parent.join("/");
9701 let parent_file = base_dir.join(format!("{parent_path}.tl"));
9702 if parent_file.exists() {
9703 return Ok(parent_file.to_string_lossy().to_string());
9704 }
9705 let parent_dir = base_dir.join(&parent_path).join("mod.tl");
9706 if parent_dir.exists() {
9707 return Ok(parent_dir.to_string_lossy().to_string());
9708 }
9709 }
9710
9711 let pkg_name_underscore = segments[0];
9714 let pkg_name_hyphen = pkg_name_underscore.replace('_', "-");
9715 let pkg_root = self
9716 .package_roots
9717 .get(pkg_name_underscore)
9718 .or_else(|| self.package_roots.get(&pkg_name_hyphen));
9719
9720 if let Some(root) = pkg_root {
9721 let remaining = &segments[1..];
9722 if let Some(path) = resolve_package_file(root, remaining) {
9723 return Ok(path);
9724 }
9725 }
9726
9727 Err(runtime_err(format!(
9728 "Module not found: `{}`",
9729 segments.join(".")
9730 )))
9731 }
9732
9733 fn call_vm_function(&mut self, func: &VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
9735 match func {
9736 VmValue::Function(closure) => {
9737 let proto = closure.prototype.clone();
9738 let arity = proto.arity as usize;
9739 if args.len() != arity {
9740 return Err(runtime_err(format!(
9741 "Expected {} arguments, got {}",
9742 arity,
9743 args.len()
9744 )));
9745 }
9746
9747 if proto.is_generator {
9749 let mut closed_upvalues = Vec::new();
9750 for uv in &closure.upvalues {
9751 match uv {
9752 UpvalueRef::Open { stack_index } => {
9753 let val = self.stack[*stack_index].clone();
9754 closed_upvalues.push(UpvalueRef::Closed(val));
9755 }
9756 UpvalueRef::Closed(v) => {
9757 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
9758 }
9759 }
9760 }
9761 let num_regs = proto.num_registers as usize;
9762 let mut saved_stack = vec![VmValue::None; num_regs];
9763 for (i, arg) in args.iter().enumerate() {
9764 saved_stack[i] = arg.clone();
9765 }
9766 let gn = VmGenerator::new(GeneratorKind::UserDefined {
9767 prototype: proto,
9768 upvalues: closed_upvalues,
9769 saved_stack,
9770 ip: 0,
9771 });
9772 return Ok(VmValue::Generator(Arc::new(Mutex::new(gn))));
9773 }
9774
9775 let new_base = self.stack.len();
9776 self.ensure_stack(new_base + proto.num_registers as usize + 1);
9777
9778 for (i, arg) in args.iter().enumerate() {
9779 self.stack[new_base + i] = arg.clone();
9780 }
9781
9782 self.frames.push(CallFrame {
9783 prototype: proto,
9784 ip: 0,
9785 base: new_base,
9786 upvalues: closure.upvalues.clone(),
9787 });
9788
9789 let result = self.run()?;
9790 self.stack.truncate(new_base);
9791 Ok(result)
9792 }
9793 VmValue::Builtin(id) => {
9794 let args_base = self.stack.len();
9796 for arg in args {
9797 self.stack.push(arg.clone());
9798 }
9799 let result = self.call_builtin(*id as u16, args_base, args.len());
9800 self.stack.truncate(args_base);
9801 result
9802 }
9803 _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
9804 }
9805 }
9806
9807 #[cfg(feature = "native")]
9810 fn handle_table_pipe(
9811 &mut self,
9812 frame_idx: usize,
9813 table_val: VmValue,
9814 op_const: u8,
9815 args_const: u8,
9816 ) -> Result<VmValue, TlError> {
9817 let df = match table_val {
9818 VmValue::Table(t) => t.df,
9819 other => {
9820 return self.table_pipe_fallback(other, frame_idx, op_const, args_const);
9822 }
9823 };
9824
9825 let frame = &self.frames[frame_idx];
9826 let op_name = match &frame.prototype.constants[op_const as usize] {
9827 Constant::String(s) => s.to_string(),
9828 _ => return Err(runtime_err("Expected string constant for table op")),
9829 };
9830 let ast_args = match &frame.prototype.constants[args_const as usize] {
9831 Constant::AstExprList(args) => args.clone(),
9832 _ => return Err(runtime_err("Expected AST expr list for table args")),
9833 };
9834
9835 let ctx = self.build_translate_context();
9836
9837 match op_name.as_str() {
9838 "filter" => {
9839 if ast_args.len() != 1 {
9840 return Err(runtime_err("filter() expects 1 argument (predicate)"));
9841 }
9842 let pred = translate_expr(&ast_args[0], &ctx).map_err(runtime_err)?;
9843 let filtered = df.filter(pred).map_err(|e| runtime_err(format!("{e}")))?;
9844 Ok(VmValue::Table(VmTable { df: filtered }))
9845 }
9846 "select" => {
9847 if ast_args.is_empty() {
9848 return Err(runtime_err("select() expects at least 1 argument"));
9849 }
9850 let mut select_exprs = Vec::new();
9851 for arg in &ast_args {
9852 match arg {
9853 AstExpr::Ident(name) => select_exprs.push(col(name.as_str())),
9854 AstExpr::NamedArg { name, value } => {
9855 let expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9856 select_exprs.push(expr.alias(name));
9857 }
9858 AstExpr::String(name) => select_exprs.push(col(name.as_str())),
9859 other => {
9860 let expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9861 select_exprs.push(expr);
9862 }
9863 }
9864 }
9865 let selected = df
9866 .select(select_exprs)
9867 .map_err(|e| runtime_err(format!("{e}")))?;
9868 Ok(VmValue::Table(VmTable { df: selected }))
9869 }
9870 "sort" => {
9871 if ast_args.is_empty() {
9872 return Err(runtime_err("sort() expects at least 1 argument (column)"));
9873 }
9874 let mut sort_exprs = Vec::new();
9875 let mut i = 0;
9876 while i < ast_args.len() {
9877 let col_name = match &ast_args[i] {
9878 AstExpr::Ident(name) => name.clone(),
9879 AstExpr::String(name) => name.clone(),
9880 _ => {
9881 return Err(runtime_err(
9882 "sort() column must be an identifier or string",
9883 ));
9884 }
9885 };
9886 i += 1;
9887 let ascending = if i < ast_args.len() {
9888 match &ast_args[i] {
9889 AstExpr::String(dir) if dir == "desc" || dir == "DESC" => {
9890 i += 1;
9891 false
9892 }
9893 AstExpr::String(dir) if dir == "asc" || dir == "ASC" => {
9894 i += 1;
9895 true
9896 }
9897 _ => true,
9898 }
9899 } else {
9900 true
9901 };
9902 sort_exprs.push(col(col_name.as_str()).sort(ascending, true));
9903 }
9904 let sorted = df
9905 .sort(sort_exprs)
9906 .map_err(|e| runtime_err(format!("{e}")))?;
9907 Ok(VmValue::Table(VmTable { df: sorted }))
9908 }
9909 "with" => {
9910 if ast_args.len() != 1 {
9911 return Err(runtime_err(
9912 "with() expects 1 argument (map of column definitions)",
9913 ));
9914 }
9915 let pairs = match &ast_args[0] {
9916 AstExpr::Map(pairs) => pairs,
9917 _ => return Err(runtime_err("with() expects a map { col = expr, ... }")),
9918 };
9919 let mut result_df = df;
9920 for (key, value_expr) in pairs {
9921 let col_name = match key {
9922 AstExpr::String(s) => s.clone(),
9923 AstExpr::Ident(s) => s.clone(),
9924 _ => return Err(runtime_err("with() key must be a string or identifier")),
9925 };
9926 let df_expr = translate_expr(value_expr, &ctx).map_err(runtime_err)?;
9927 result_df = result_df
9928 .with_column(&col_name, df_expr)
9929 .map_err(|e| runtime_err(format!("{e}")))?;
9930 }
9931 Ok(VmValue::Table(VmTable { df: result_df }))
9932 }
9933 "aggregate" => {
9934 let mut group_by_cols: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9935 let mut agg_exprs: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9936 for arg in &ast_args {
9937 match arg {
9938 AstExpr::NamedArg { name, value } if name == "by" => match value.as_ref() {
9939 AstExpr::String(col_name) => group_by_cols.push(col(col_name.as_str())),
9940 AstExpr::Ident(col_name) => group_by_cols.push(col(col_name.as_str())),
9941 AstExpr::List(items) => {
9942 for item in items {
9943 match item {
9944 AstExpr::String(s) => group_by_cols.push(col(s.as_str())),
9945 AstExpr::Ident(s) => group_by_cols.push(col(s.as_str())),
9946 _ => {
9947 return Err(runtime_err(
9948 "by: list items must be strings or identifiers",
9949 ));
9950 }
9951 }
9952 }
9953 }
9954 _ => return Err(runtime_err("by: must be a column name or list")),
9955 },
9956 AstExpr::NamedArg { name, value } => {
9957 let agg_expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9958 agg_exprs.push(agg_expr.alias(name));
9959 }
9960 other => {
9961 let agg_expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9962 agg_exprs.push(agg_expr);
9963 }
9964 }
9965 }
9966 let aggregated = df
9967 .aggregate(group_by_cols, agg_exprs)
9968 .map_err(|e| runtime_err(format!("{e}")))?;
9969 Ok(VmValue::Table(VmTable { df: aggregated }))
9970 }
9971 "join" => {
9972 if ast_args.is_empty() {
9973 return Err(runtime_err(
9974 "join() expects at least 1 argument (right table)",
9975 ));
9976 }
9977 let right_table = self.eval_ast_to_vm(&ast_args[0])?;
9979 let right_df = match right_table {
9980 VmValue::Table(t) => t.df,
9981 _ => return Err(runtime_err("join() first arg must be a table")),
9982 };
9983 let mut left_cols: Vec<String> = Vec::new();
9984 let mut right_cols: Vec<String> = Vec::new();
9985 let mut join_type = JoinType::Inner;
9986 for arg in &ast_args[1..] {
9987 match arg {
9988 AstExpr::NamedArg { name, value } if name == "on" => {
9989 if let AstExpr::BinOp {
9990 left,
9991 op: tl_ast::BinOp::Eq,
9992 right,
9993 } = value.as_ref()
9994 {
9995 let lc = match left.as_ref() {
9996 AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
9997 _ => {
9998 return Err(runtime_err(
9999 "on: left side must be a column name",
10000 ));
10001 }
10002 };
10003 let rc = match right.as_ref() {
10004 AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10005 _ => {
10006 return Err(runtime_err(
10007 "on: right side must be a column name",
10008 ));
10009 }
10010 };
10011 left_cols.push(lc);
10012 right_cols.push(rc);
10013 }
10014 }
10015 AstExpr::NamedArg { name, value } if name == "kind" => {
10016 if let AstExpr::String(kind_str) = value.as_ref() {
10017 join_type = match kind_str.as_str() {
10018 "inner" => JoinType::Inner,
10019 "left" => JoinType::Left,
10020 "right" => JoinType::Right,
10021 "full" => JoinType::Full,
10022 _ => {
10023 return Err(runtime_err(format!(
10024 "Unknown join type: {kind_str}"
10025 )));
10026 }
10027 };
10028 }
10029 }
10030 _ => {}
10031 }
10032 }
10033 let lc_refs: Vec<&str> = left_cols.iter().map(|s| s.as_str()).collect();
10034 let rc_refs: Vec<&str> = right_cols.iter().map(|s| s.as_str()).collect();
10035 let joined = df
10036 .join(right_df, join_type, &lc_refs, &rc_refs, None)
10037 .map_err(|e| runtime_err(format!("{e}")))?;
10038 Ok(VmValue::Table(VmTable { df: joined }))
10039 }
10040 "head" | "limit" => {
10041 let n = match ast_args.first() {
10042 Some(AstExpr::Int(n)) => *n as usize,
10043 None => 10,
10044 _ => return Err(runtime_err("head/limit expects an integer")),
10045 };
10046 let limited = df
10047 .limit(0, Some(n))
10048 .map_err(|e| runtime_err(format!("{e}")))?;
10049 Ok(VmValue::Table(VmTable { df: limited }))
10050 }
10051 "collect" => {
10052 let batches = self.engine().collect(df).map_err(runtime_err)?;
10053 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10054 Ok(VmValue::String(Arc::from(formatted.as_str())))
10055 }
10056 "show" => {
10057 let limit = match ast_args.first() {
10058 Some(AstExpr::Int(n)) => *n as usize,
10059 None => 20,
10060 _ => 20,
10061 };
10062 let limited = df
10063 .limit(0, Some(limit))
10064 .map_err(|e| runtime_err(format!("{e}")))?;
10065 let batches = self.engine().collect(limited).map_err(runtime_err)?;
10066 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10067 println!("{formatted}");
10068 self.output.push(formatted);
10069 Ok(VmValue::None)
10070 }
10071 "describe" => {
10072 let schema = df.schema();
10073 let mut lines = Vec::new();
10074 lines.push("Columns:".to_string());
10075 for field in schema.fields() {
10076 lines.push(format!(" {}: {}", field.name(), field.data_type()));
10077 }
10078 let output = lines.join("\n");
10079 println!("{output}");
10080 self.output.push(output.clone());
10081 Ok(VmValue::String(Arc::from(output.as_str())))
10082 }
10083 "write_csv" => {
10084 if ast_args.len() != 1 {
10085 return Err(runtime_err("write_csv() expects 1 argument (path)"));
10086 }
10087 let path = self.eval_ast_to_string(&ast_args[0])?;
10088 self.engine().write_csv(df, &path).map_err(runtime_err)?;
10089 Ok(VmValue::None)
10090 }
10091 "write_parquet" => {
10092 if ast_args.len() != 1 {
10093 return Err(runtime_err("write_parquet() expects 1 argument (path)"));
10094 }
10095 let path = self.eval_ast_to_string(&ast_args[0])?;
10096 self.engine()
10097 .write_parquet(df, &path)
10098 .map_err(runtime_err)?;
10099 Ok(VmValue::None)
10100 }
10101 "fill_null" => {
10103 if ast_args.is_empty() {
10104 return Err(runtime_err(
10105 "fill_null() expects (column, [strategy/value])",
10106 ));
10107 }
10108 let column = self.eval_ast_to_string(&ast_args[0])?;
10109 if ast_args.len() >= 2 {
10110 let val = self.eval_ast_to_vm(&ast_args[1])?;
10111 match val {
10112 VmValue::String(s) => {
10113 let fill_val = if ast_args.len() >= 3 {
10115 match self.eval_ast_to_vm(&ast_args[2])? {
10116 VmValue::Int(n) => Some(n as f64),
10117 VmValue::Float(f) => Some(f),
10118 _ => None,
10119 }
10120 } else {
10121 None
10122 };
10123 let result = self
10124 .engine()
10125 .fill_null(df, &column, &s, fill_val)
10126 .map_err(runtime_err)?;
10127 Ok(VmValue::Table(VmTable { df: result }))
10128 }
10129 VmValue::Int(n) => {
10130 let result = self
10131 .engine()
10132 .fill_null(df, &column, "value", Some(n as f64))
10133 .map_err(runtime_err)?;
10134 Ok(VmValue::Table(VmTable { df: result }))
10135 }
10136 VmValue::Float(f) => {
10137 let result = self
10138 .engine()
10139 .fill_null(df, &column, "value", Some(f))
10140 .map_err(runtime_err)?;
10141 Ok(VmValue::Table(VmTable { df: result }))
10142 }
10143 _ => Err(runtime_err(
10144 "fill_null() second arg must be a strategy or fill value",
10145 )),
10146 }
10147 } else {
10148 let result = self
10149 .engine()
10150 .fill_null(df, &column, "zero", None)
10151 .map_err(runtime_err)?;
10152 Ok(VmValue::Table(VmTable { df: result }))
10153 }
10154 }
10155 "drop_null" => {
10156 if ast_args.is_empty() {
10157 return Err(runtime_err("drop_null() expects (column)"));
10158 }
10159 let column = self.eval_ast_to_string(&ast_args[0])?;
10160 let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
10161 Ok(VmValue::Table(VmTable { df: result }))
10162 }
10163 "dedup" => {
10164 let columns: Vec<String> = ast_args
10165 .iter()
10166 .filter_map(|a| self.eval_ast_to_string(a).ok())
10167 .collect();
10168 let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
10169 Ok(VmValue::Table(VmTable { df: result }))
10170 }
10171 "clamp" => {
10172 if ast_args.len() < 3 {
10173 return Err(runtime_err("clamp() expects (column, min, max)"));
10174 }
10175 let column = self.eval_ast_to_string(&ast_args[0])?;
10176 let min_val = match self.eval_ast_to_vm(&ast_args[1])? {
10177 VmValue::Int(n) => n as f64,
10178 VmValue::Float(f) => f,
10179 _ => return Err(runtime_err("clamp() min must be a number")),
10180 };
10181 let max_val = match self.eval_ast_to_vm(&ast_args[2])? {
10182 VmValue::Int(n) => n as f64,
10183 VmValue::Float(f) => f,
10184 _ => return Err(runtime_err("clamp() max must be a number")),
10185 };
10186 let result = self
10187 .engine()
10188 .clamp(df, &column, min_val, max_val)
10189 .map_err(runtime_err)?;
10190 Ok(VmValue::Table(VmTable { df: result }))
10191 }
10192 "data_profile" => {
10193 let result = self.engine().data_profile(df).map_err(runtime_err)?;
10194 Ok(VmValue::Table(VmTable { df: result }))
10195 }
10196 "row_count" => {
10197 let count = self.engine().row_count(df).map_err(runtime_err)?;
10198 Ok(VmValue::Int(count))
10199 }
10200 "null_rate" => {
10201 if ast_args.is_empty() {
10202 return Err(runtime_err("null_rate() expects (column)"));
10203 }
10204 let column = self.eval_ast_to_string(&ast_args[0])?;
10205 let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
10206 Ok(VmValue::Float(rate))
10207 }
10208 "is_unique" => {
10209 if ast_args.is_empty() {
10210 return Err(runtime_err("is_unique() expects (column)"));
10211 }
10212 let column = self.eval_ast_to_string(&ast_args[0])?;
10213 let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
10214 Ok(VmValue::Bool(unique))
10215 }
10216 "window" => {
10218 use tl_data::datafusion::logical_expr::{
10219 WindowFrame, WindowFunctionDefinition,
10220 expr::{Sort as DfSort, WindowFunction as WinFunc},
10221 };
10222 if ast_args.is_empty() {
10223 return Err(runtime_err(
10224 "window() expects named arguments: fn, partition_by, order_by, alias",
10225 ));
10226 }
10227 let mut win_fn_name = String::new();
10228 let mut partition_by_cols: Vec<String> = Vec::new();
10229 let mut order_by_cols: Vec<String> = Vec::new();
10230 let mut alias_name = String::new();
10231 let mut win_args: Vec<String> = Vec::new();
10232 let mut descending = false;
10233
10234 for arg in &ast_args {
10235 if let AstExpr::NamedArg { name, value } = arg {
10236 match name.as_str() {
10237 "fn" => win_fn_name = self.eval_ast_to_string(value)?,
10238 "partition_by" => match value.as_ref() {
10239 AstExpr::List(items) => {
10240 for item in items {
10241 partition_by_cols.push(self.eval_ast_to_string(item)?);
10242 }
10243 }
10244 _ => partition_by_cols.push(self.eval_ast_to_string(value)?),
10245 },
10246 "order_by" => match value.as_ref() {
10247 AstExpr::List(items) => {
10248 for item in items {
10249 order_by_cols.push(self.eval_ast_to_string(item)?);
10250 }
10251 }
10252 _ => order_by_cols.push(self.eval_ast_to_string(value)?),
10253 },
10254 "alias" | "as" => alias_name = self.eval_ast_to_string(value)?,
10255 "args" => match value.as_ref() {
10256 AstExpr::List(items) => {
10257 for item in items {
10258 win_args.push(self.eval_ast_to_string(item)?);
10259 }
10260 }
10261 _ => win_args.push(self.eval_ast_to_string(value)?),
10262 },
10263 "desc" => {
10264 if let AstExpr::Bool(b) = value.as_ref() {
10265 descending = *b;
10266 }
10267 }
10268 _ => {}
10269 }
10270 }
10271 }
10272
10273 if win_fn_name.is_empty() {
10274 return Err(runtime_err(
10275 "window() requires fn: parameter (rank, row_number, dense_rank, lag, lead, ntile)",
10276 ));
10277 }
10278 if alias_name.is_empty() {
10279 alias_name = win_fn_name.clone();
10280 }
10281
10282 let session = self.engine().session_ctx();
10284 let win_udf = match win_fn_name.as_str() {
10285 "rank" => session.udwf("rank"),
10286 "dense_rank" => session.udwf("dense_rank"),
10287 "row_number" => session.udwf("row_number"),
10288 "percent_rank" => session.udwf("percent_rank"),
10289 "cume_dist" => session.udwf("cume_dist"),
10290 "ntile" => session.udwf("ntile"),
10291 "lag" => session.udwf("lag"),
10292 "lead" => session.udwf("lead"),
10293 "first_value" => session.udwf("first_value"),
10294 "last_value" => session.udwf("last_value"),
10295 _ => {
10296 return Err(runtime_err(format!(
10297 "Unknown window function: {win_fn_name}"
10298 )));
10299 }
10300 }
10301 .map_err(|e| {
10302 runtime_err(format!(
10303 "Window function '{win_fn_name}' not available: {e}"
10304 ))
10305 })?;
10306
10307 let fun = WindowFunctionDefinition::WindowUDF(win_udf);
10308
10309 let func_args: Vec<tl_data::datafusion::prelude::Expr> = win_args
10311 .iter()
10312 .map(|a| {
10313 if let Ok(n) = a.parse::<i64>() {
10314 lit(n)
10315 } else {
10316 col(a.as_str())
10317 }
10318 })
10319 .collect();
10320
10321 let partition_exprs: Vec<tl_data::datafusion::prelude::Expr> =
10322 partition_by_cols.iter().map(|c| col(c.as_str())).collect();
10323 let order_exprs: Vec<DfSort> = order_by_cols
10324 .iter()
10325 .map(|c| DfSort::new(col(c.as_str()), !descending, true))
10326 .collect();
10327
10328 let has_order = !order_exprs.is_empty();
10329 let win_expr = tl_data::datafusion::prelude::Expr::WindowFunction(WinFunc {
10330 fun,
10331 args: func_args,
10332 partition_by: partition_exprs,
10333 order_by: order_exprs,
10334 window_frame: WindowFrame::new(if has_order { Some(true) } else { None }),
10335 null_treatment: None,
10336 })
10337 .alias(&alias_name);
10338
10339 let schema = df.schema();
10341 let mut select_exprs: Vec<tl_data::datafusion::prelude::Expr> = schema
10342 .fields()
10343 .iter()
10344 .map(|f| col(f.name().as_str()))
10345 .collect();
10346 select_exprs.push(win_expr);
10347
10348 let result_df = df
10349 .select(select_exprs)
10350 .map_err(|e| runtime_err(format!("Window function error: {e}")))?;
10351 Ok(VmValue::Table(VmTable { df: result_df }))
10352 }
10353 "union" => {
10355 if ast_args.is_empty() {
10356 return Err(runtime_err("union() expects a table argument"));
10357 }
10358 let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10359 let right_df = match right_table {
10360 VmValue::Table(t) => t.df,
10361 _ => return Err(runtime_err("union() argument must be a table")),
10362 };
10363 let result_df = df
10364 .union(right_df)
10365 .map_err(|e| runtime_err(format!("Union error: {e}")))?;
10366 Ok(VmValue::Table(VmTable { df: result_df }))
10367 }
10368 "sample" => {
10370 use tl_data::datafusion::arrow::{array::UInt32Array, compute};
10371 use tl_data::datafusion::datasource::MemTable;
10372 if ast_args.is_empty() {
10373 return Err(runtime_err("sample() expects a count or fraction"));
10374 }
10375 let batches = self.engine().collect(df).map_err(runtime_err)?;
10376 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10377 let sample_count = match &ast_args[0] {
10378 AstExpr::Int(n) => (*n as usize).min(total_rows),
10379 AstExpr::Float(f) if *f > 0.0 && *f <= 1.0 => {
10380 ((total_rows as f64) * f).ceil() as usize
10381 }
10382 _ => {
10383 let val = self.eval_ast_to_string(&ast_args[0])?;
10384 val.parse::<usize>().map_err(|_| {
10385 runtime_err("sample() expects integer count or float fraction")
10386 })?
10387 }
10388 };
10389 if total_rows == 0 || sample_count == 0 {
10390 let schema = batches[0].schema();
10391 let empty = tl_data::datafusion::arrow::record_batch::RecordBatch::new_empty(
10392 schema.clone(),
10393 );
10394 let mem_table = MemTable::try_new(schema, vec![vec![empty]])
10395 .map_err(|e| runtime_err(format!("{e}")))?;
10396 let new_df = self
10397 .engine()
10398 .session_ctx()
10399 .read_table(Arc::new(mem_table))
10400 .map_err(|e| runtime_err(format!("{e}")))?;
10401 return Ok(VmValue::Table(VmTable { df: new_df }));
10402 }
10403 let mut rng = rand::thread_rng();
10405 let mut indices: Vec<usize> = (0..total_rows).collect();
10406 use rand::seq::SliceRandom;
10407 indices.partial_shuffle(&mut rng, sample_count);
10408 indices.truncate(sample_count);
10409 indices.sort();
10410 let combined = compute::concat_batches(&batches[0].schema(), &batches)
10412 .map_err(|e| runtime_err(format!("{e}")))?;
10413 let idx_array =
10414 UInt32Array::from(indices.iter().map(|&i| i as u32).collect::<Vec<_>>());
10415 let sampled_cols: Vec<tl_data::datafusion::arrow::array::ArrayRef> = (0..combined
10416 .num_columns())
10417 .map(|c| {
10418 compute::take(combined.column(c), &idx_array, None)
10419 .map_err(|e| runtime_err(format!("{e}")))
10420 })
10421 .collect::<Result<Vec<_>, _>>()?;
10422 let sampled_batch = tl_data::datafusion::arrow::record_batch::RecordBatch::try_new(
10423 combined.schema(),
10424 sampled_cols,
10425 )
10426 .map_err(|e| runtime_err(format!("{e}")))?;
10427 let mem_table =
10428 MemTable::try_new(sampled_batch.schema(), vec![vec![sampled_batch]])
10429 .map_err(|e| runtime_err(format!("{e}")))?;
10430 let new_df = self
10431 .engine()
10432 .session_ctx()
10433 .read_table(Arc::new(mem_table))
10434 .map_err(|e| runtime_err(format!("{e}")))?;
10435 Ok(VmValue::Table(VmTable { df: new_df }))
10436 }
10437 _ => Err(runtime_err(format!("Unknown table operation: {op_name}"))),
10438 }
10439 }
10440
10441 fn table_pipe_fallback(
10444 &mut self,
10445 left_val: VmValue,
10446 frame_idx: usize,
10447 op_const: u8,
10448 args_const: u8,
10449 ) -> Result<VmValue, TlError> {
10450 let frame = &self.frames[frame_idx];
10451 let op_name = match &frame.prototype.constants[op_const as usize] {
10452 Constant::String(s) => s.to_string(),
10453 _ => return Err(runtime_err("Expected string constant for table op")),
10454 };
10455 let ast_args = match &frame.prototype.constants[args_const as usize] {
10456 Constant::AstExprList(args) => args.clone(),
10457 _ => return Err(runtime_err("Expected AST expr list for table args")),
10458 };
10459
10460 if let Some(builtin_id) = BuiltinId::from_name(&op_name) {
10462 let mut all_args = vec![left_val];
10464 for arg in &ast_args {
10465 all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10466 }
10467 let args_base = self.stack.len();
10468 for arg in &all_args {
10469 self.stack.push(arg.clone());
10470 }
10471 let result = self.call_builtin(builtin_id as u16, args_base, all_args.len());
10472 self.stack.truncate(args_base);
10473 return result;
10474 }
10475
10476 if let Some(func) = self.globals.get(&op_name).cloned() {
10478 let mut all_args = vec![left_val];
10479 for arg in &ast_args {
10480 all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10481 }
10482 return self.call_vm_function(&func, &all_args);
10483 }
10484
10485 Err(runtime_err(format!("Unknown operation: `{op_name}`")))
10486 }
10487
10488 #[cfg(feature = "native")]
10490 fn build_translate_context(&self) -> TranslateContext {
10491 let mut ctx = TranslateContext::new();
10492 for (name, val) in &self.globals {
10494 let local = match val {
10495 VmValue::Int(n) => Some(LocalValue::Int(*n)),
10496 VmValue::Float(f) => Some(LocalValue::Float(*f)),
10497 VmValue::String(s) => Some(LocalValue::String(s.to_string())),
10498 VmValue::Bool(b) => Some(LocalValue::Bool(*b)),
10499 _ => None,
10500 };
10501 if let Some(l) = local {
10502 ctx.locals.insert(name.clone(), l);
10503 }
10504 }
10505 if let Some(frame) = self.frames.last() {
10507 for local_idx in 0..frame.prototype.num_locals as usize {
10508 if let Some(val) = self.stack.get(frame.base + local_idx) {
10509 let _ = val;
10511 }
10512 }
10513 }
10514 ctx
10515 }
10516
10517 fn eval_ast_to_vm(&mut self, expr: &AstExpr) -> Result<VmValue, TlError> {
10520 match expr {
10521 AstExpr::Ident(name) => {
10522 if let Some(val) = self.globals.get(name) {
10524 return Ok(val.clone());
10525 }
10526 if let Some(frame) = self.frames.last() {
10528 for i in 0..frame.prototype.num_registers as usize {
10529 if let Some(val) = self.stack.get(frame.base + i)
10530 && !matches!(val, VmValue::None)
10531 {
10532 }
10535 }
10536 }
10537 Err(runtime_err(format!("Undefined variable: `{name}`")))
10538 }
10539 AstExpr::String(s) => Ok(VmValue::String(Arc::from(s.as_str()))),
10540 AstExpr::Int(n) => Ok(VmValue::Int(*n)),
10541 AstExpr::Float(f) => Ok(VmValue::Float(*f)),
10542 AstExpr::Bool(b) => Ok(VmValue::Bool(*b)),
10543 AstExpr::None => Ok(VmValue::None),
10544 AstExpr::Closure {
10545 params: _, body: _, ..
10546 } => {
10547 use crate::compiler;
10548 let wrapper = tl_ast::Program {
10549 statements: vec![tl_ast::Stmt {
10550 kind: tl_ast::StmtKind::Expr(expr.clone()),
10551 span: tl_errors::Span::new(0, 0),
10552 doc_comment: None,
10553 }],
10554 module_doc: None,
10555 };
10556 let proto = compiler::compile(&wrapper)?;
10557 let mut temp_vm = Vm::new();
10558 temp_vm.globals = self.globals.clone();
10560 let result = temp_vm.execute(&proto)?;
10561 Ok(result)
10562 }
10563 _ => {
10564 let wrapper = tl_ast::Program {
10566 statements: vec![tl_ast::Stmt {
10567 kind: tl_ast::StmtKind::Expr(expr.clone()),
10568 span: tl_errors::Span::new(0, 0),
10569 doc_comment: None,
10570 }],
10571 module_doc: None,
10572 };
10573 use crate::compiler;
10574 let proto = compiler::compile(&wrapper)?;
10575 let mut temp_vm = Vm::new();
10576 temp_vm.globals = self.globals.clone();
10577 temp_vm.execute(&proto)
10578 }
10579 }
10580 }
10581
10582 fn eval_ast_to_string(&mut self, expr: &AstExpr) -> Result<String, TlError> {
10583 match self.eval_ast_to_vm(expr)? {
10584 VmValue::String(s) => Ok(s.to_string()),
10585 _ => Err(runtime_err("Expected a string")),
10586 }
10587 }
10588
10589 fn interpolate_string(&self, s: &str, _base: usize) -> Result<String, TlError> {
10591 let mut result = String::new();
10592 let mut chars = s.chars().peekable();
10593 while let Some(ch) = chars.next() {
10594 if ch == '{' {
10595 let mut var_name = String::new();
10596 let mut depth = 1;
10597 for c in chars.by_ref() {
10598 if c == '{' {
10599 depth += 1;
10600 } else if c == '}' {
10601 depth -= 1;
10602 if depth == 0 {
10603 break;
10604 }
10605 }
10606 var_name.push(c);
10607 }
10608 if let Some(val) = self.globals.get(&var_name) {
10610 result.push_str(&format!("{val}"));
10611 } else {
10612 result.push('{');
10616 result.push_str(&var_name);
10617 result.push('}');
10618 }
10619 } else if ch == '\\' {
10620 match chars.next() {
10621 Some('n') => result.push('\n'),
10622 Some('t') => result.push('\t'),
10623 Some('\\') => result.push('\\'),
10624 Some('"') => result.push('"'),
10625 Some(c) => {
10626 result.push('\\');
10627 result.push(c);
10628 }
10629 None => result.push('\\'),
10630 }
10631 } else {
10632 result.push(ch);
10633 }
10634 }
10635 Ok(result)
10636 }
10637
10638 pub fn execute_single_instruction(
10641 &mut self,
10642 inst: u32,
10643 proto: &Prototype,
10644 base: usize,
10645 ) -> Result<Option<VmValue>, TlError> {
10646 use crate::opcode::{decode_a, decode_b, decode_bx, decode_c, decode_op};
10647
10648 let proto = Arc::new(proto.clone());
10649 self.frames.push(CallFrame {
10651 prototype: proto.clone(),
10652 ip: 0,
10653 base,
10654 upvalues: Vec::new(),
10655 });
10656 let frame_idx = self.frames.len() - 1;
10657
10658 let op = decode_op(inst);
10659 let a = decode_a(inst);
10660 let _b = decode_b(inst);
10661 let _c = decode_c(inst);
10662 let bx = decode_bx(inst);
10663
10664 let result = match op {
10667 Op::GetGlobal => {
10668 let name = self.get_string_constant(frame_idx, bx)?;
10669 let val = self
10670 .globals
10671 .get(name.as_ref())
10672 .cloned()
10673 .unwrap_or(VmValue::None);
10674 self.stack[base + a as usize] = val;
10675 Ok(None)
10676 }
10677 Op::SetGlobal => {
10678 let name = self.get_string_constant(frame_idx, bx)?;
10679 let val = self.stack[base + a as usize].clone();
10680 self.globals.insert(name.to_string(), val);
10681 Ok(None)
10682 }
10683 _ => {
10684 Ok(None)
10687 }
10688 };
10689
10690 self.frames.pop();
10691 result
10692 }
10693}
10694
10695impl Default for Vm {
10696 fn default() -> Self {
10697 Self::new()
10698 }
10699}
10700
10701#[cfg(test)]
10702mod tests {
10703 use super::*;
10704 use crate::compiler::compile;
10705 use tl_parser::parse;
10706
10707 fn run(source: &str) -> Result<VmValue, TlError> {
10708 let program = parse(source)?;
10709 let proto = compile(&program)?;
10710 let mut vm = Vm::new();
10711 vm.execute(&proto)
10712 }
10713
10714 fn run_output(source: &str) -> Vec<String> {
10715 let program = parse(source).unwrap();
10716 let proto = compile(&program).unwrap();
10717 let mut vm = Vm::new();
10718 vm.execute(&proto).unwrap();
10719 vm.output
10720 }
10721
10722 #[test]
10723 fn test_vm_arithmetic() {
10724 assert!(matches!(run("1 + 2").unwrap(), VmValue::Int(3)));
10725 assert!(matches!(run("10 - 3").unwrap(), VmValue::Int(7)));
10726 assert!(matches!(run("4 * 5").unwrap(), VmValue::Int(20)));
10727 assert!(matches!(run("10 / 3").unwrap(), VmValue::Int(3)));
10728 assert!(matches!(run("10 % 3").unwrap(), VmValue::Int(1)));
10729 assert!(matches!(run("2 ** 10").unwrap(), VmValue::Int(1024)));
10730 let output = run_output("print(1 + 2)");
10731 assert_eq!(output, vec!["3"]);
10732 }
10733
10734 #[test]
10735 fn test_vm_let_and_print() {
10736 let output = run_output("let x = 42\nprint(x)");
10737 assert_eq!(output, vec!["42"]);
10738 }
10739
10740 #[test]
10741 fn test_vm_function() {
10742 let output = run_output("fn double(n) { n * 2 }\nlet result = double(21)\nprint(result)");
10743 assert_eq!(output, vec!["42"]);
10744 }
10745
10746 #[test]
10747 fn test_vm_if_else() {
10748 let output =
10749 run_output("let x = 10\nif x > 5 { print(\"big\") } else { print(\"small\") }");
10750 assert_eq!(output, vec!["big"]);
10751 }
10752
10753 #[test]
10754 fn test_vm_list() {
10755 let output = run_output("let items = [1, 2, 3]\nprint(len(items))");
10756 assert_eq!(output, vec!["3"]);
10757 }
10758
10759 #[test]
10760 fn test_vm_map_builtin() {
10761 let output = run_output(
10762 "let nums = [1, 2, 3]\nlet doubled = map(nums, (x) => x * 2)\nprint(doubled)",
10763 );
10764 assert_eq!(output, vec!["[2, 4, 6]"]);
10765 }
10766
10767 #[test]
10768 fn test_vm_filter_builtin() {
10769 let output = run_output(
10770 "let nums = [1, 2, 3, 4, 5]\nlet evens = filter(nums, (x) => x % 2 == 0)\nprint(evens)",
10771 );
10772 assert_eq!(output, vec!["[2, 4]"]);
10773 }
10774
10775 #[test]
10776 fn test_vm_for_loop() {
10777 let output = run_output("let sum = 0\nfor i in range(5) { sum = sum + i }\nprint(sum)");
10778 assert_eq!(output, vec!["10"]);
10779 }
10780
10781 #[test]
10782 fn test_vm_closure() {
10783 let output = run_output("let double = (x) => x * 2\nprint(double(5))");
10784 assert_eq!(output, vec!["10"]);
10785 }
10786
10787 #[test]
10788 fn test_vm_sum() {
10789 let output = run_output("print(sum([1, 2, 3, 4]))");
10790 assert_eq!(output, vec!["10"]);
10791 }
10792
10793 #[test]
10794 fn test_vm_reduce() {
10795 let output = run_output(
10796 "let product = reduce([1, 2, 3, 4], 1, (acc, x) => acc * x)\nprint(product)",
10797 );
10798 assert_eq!(output, vec!["24"]);
10799 }
10800
10801 #[test]
10802 fn test_vm_pipe() {
10803 let output = run_output("let result = [1, 2, 3] |> map((x) => x + 10)\nprint(result)");
10804 assert_eq!(output, vec!["[11, 12, 13]"]);
10805 }
10806
10807 #[test]
10808 fn test_vm_comparison() {
10809 let output = run_output("print(5 > 3)");
10810 assert_eq!(output, vec!["true"]);
10811 }
10812
10813 #[test]
10814 fn test_vm_precedence() {
10815 let output = run_output("print(2 + 3 * 4)");
10816 assert_eq!(output, vec!["14"]);
10817 }
10818
10819 #[test]
10820 fn test_vm_match() {
10821 let output =
10822 run_output("let x = 2\nprint(match x { 1 => \"one\", 2 => \"two\", _ => \"other\" })");
10823 assert_eq!(output, vec!["two"]);
10824 }
10825
10826 #[test]
10827 fn test_vm_match_wildcard() {
10828 let output = run_output("print(match 99 { 1 => \"one\", _ => \"other\" })");
10829 assert_eq!(output, vec!["other"]);
10830 }
10831
10832 #[test]
10833 fn test_vm_match_binding() {
10834 let output = run_output("print(match 42 { val => val + 1 })");
10835 assert_eq!(output, vec!["43"]);
10836 }
10837
10838 #[test]
10839 fn test_vm_match_guard() {
10840 let output = run_output(
10841 "let x = 5\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10842 );
10843 assert_eq!(output, vec!["pos"]);
10844 }
10845
10846 #[test]
10847 fn test_vm_match_guard_negative() {
10848 let output = run_output(
10849 "let x = -3\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10850 );
10851 assert_eq!(output, vec!["neg"]);
10852 }
10853
10854 #[test]
10855 fn test_vm_match_guard_zero() {
10856 let output = run_output(
10857 "let x = 0\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10858 );
10859 assert_eq!(output, vec!["zero"]);
10860 }
10861
10862 #[test]
10863 fn test_vm_match_enum_destructure() {
10864 let output = run_output(
10865 r#"
10866enum Shape { Circle(int64), Rect(int64, int64) }
10867let s = Shape::Circle(5)
10868print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10869"#,
10870 );
10871 assert_eq!(output, vec!["5"]);
10872 }
10873
10874 #[test]
10875 fn test_vm_match_enum_destructure_rect() {
10876 let output = run_output(
10877 r#"
10878enum Shape { Circle(int64), Rect(int64, int64) }
10879let s = Shape::Rect(3, 4)
10880print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10881"#,
10882 );
10883 assert_eq!(output, vec!["12"]);
10884 }
10885
10886 #[test]
10887 fn test_vm_match_enum_wildcard_field() {
10888 let output = run_output(
10889 r#"
10890enum Pair { Two(int64, int64) }
10891let p = Pair::Two(10, 20)
10892print(match p { Pair::Two(_, y) => y, _ => 0 })
10893"#,
10894 );
10895 assert_eq!(output, vec!["20"]);
10896 }
10897
10898 #[test]
10899 fn test_vm_match_enum_guard() {
10900 let output = run_output(
10901 r#"
10902enum Result { Ok(int64), Err(string) }
10903let r = Result::Ok(150)
10904print(match r { Result::Ok(v) if v > 100 => "big", Result::Ok(v) => "small", Result::Err(e) => e, _ => "unknown" })
10905"#,
10906 );
10907 assert_eq!(output, vec!["big"]);
10908 }
10909
10910 #[test]
10911 fn test_vm_match_or_pattern() {
10912 let output =
10913 run_output("let x = 2\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10914 assert_eq!(output, vec!["small"]);
10915 }
10916
10917 #[test]
10918 fn test_vm_match_or_pattern_no_match() {
10919 let output =
10920 run_output("let x = 10\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10921 assert_eq!(output, vec!["big"]);
10922 }
10923
10924 #[test]
10925 fn test_vm_match_string() {
10926 let output = run_output(
10927 r#"let s = "hello"
10928print(match s { "hi" => 1, "hello" => 2, _ => 0 })"#,
10929 );
10930 assert_eq!(output, vec!["2"]);
10931 }
10932
10933 #[test]
10934 fn test_vm_match_bool() {
10935 let output = run_output("print(match true { true => \"yes\", false => \"no\" })");
10936 assert_eq!(output, vec!["yes"]);
10937 }
10938
10939 #[test]
10940 fn test_vm_match_none() {
10941 let output = run_output("print(match none { none => \"nothing\", _ => \"something\" })");
10942 assert_eq!(output, vec!["nothing"]);
10943 }
10944
10945 #[test]
10946 fn test_vm_let_destructure_list() {
10947 let output = run_output("let [a, b, c] = [1, 2, 3]\nprint(a)\nprint(b)\nprint(c)");
10948 assert_eq!(output, vec!["1", "2", "3"]);
10949 }
10950
10951 #[test]
10952 fn test_vm_let_destructure_list_rest() {
10953 let output =
10954 run_output("let [head, ...tail] = [1, 2, 3, 4]\nprint(head)\nprint(len(tail))");
10955 assert_eq!(output, vec!["1", "3"]);
10956 }
10957
10958 #[test]
10959 fn test_vm_let_destructure_struct() {
10960 let output = run_output(
10961 r#"
10962struct Point { x: int64, y: int64 }
10963let p = Point { x: 10, y: 20 }
10964let Point { x, y } = p
10965print(x)
10966print(y)
10967"#,
10968 );
10969 assert_eq!(output, vec!["10", "20"]);
10970 }
10971
10972 #[test]
10973 fn test_vm_let_destructure_struct_anon() {
10974 let output = run_output(
10975 r#"
10976struct Point { x: int64, y: int64 }
10977let p = Point { x: 10, y: 20 }
10978let { x, y } = p
10979print(x)
10980print(y)
10981"#,
10982 );
10983 assert_eq!(output, vec!["10", "20"]);
10984 }
10985
10986 #[test]
10987 fn test_vm_match_struct_pattern() {
10988 let output = run_output(
10989 r#"
10990struct Point { x: int64, y: int64 }
10991let p = Point { x: 1, y: 2 }
10992print(match p { Point { x, y } => x + y, _ => 0 })
10993"#,
10994 );
10995 assert_eq!(output, vec!["3"]);
10996 }
10997
10998 #[test]
10999 fn test_vm_match_list_pattern() {
11000 let output = run_output(
11001 r#"
11002let lst = [1, 2, 3]
11003print(match lst { [a, b, c] => a + b + c, _ => 0 })
11004"#,
11005 );
11006 assert_eq!(output, vec!["6"]);
11007 }
11008
11009 #[test]
11010 fn test_vm_match_list_rest_pattern() {
11011 let output = run_output(
11012 r#"
11013let lst = [10, 20, 30, 40]
11014print(match lst { [head, ...rest] => head, _ => 0 })
11015"#,
11016 );
11017 assert_eq!(output, vec!["10"]);
11018 }
11019
11020 #[test]
11021 fn test_vm_match_list_empty() {
11022 let output = run_output(
11023 r#"
11024let lst = []
11025print(match lst { [] => "empty", _ => "nonempty" })
11026"#,
11027 );
11028 assert_eq!(output, vec!["empty"]);
11029 }
11030
11031 #[test]
11032 fn test_vm_match_list_length_mismatch() {
11033 let output = run_output(
11034 r#"
11035let lst = [1, 2, 3]
11036print(match lst { [a, b] => "two", [a, b, c] => "three", _ => "other" })
11037"#,
11038 );
11039 assert_eq!(output, vec!["three"]);
11040 }
11041
11042 #[test]
11043 fn test_vm_match_negative_literal() {
11044 let output =
11045 run_output("print(match -1 { -1 => \"neg one\", 0 => \"zero\", _ => \"other\" })");
11046 assert_eq!(output, vec!["neg one"]);
11047 }
11048
11049 #[test]
11050 fn test_vm_case_with_pattern() {
11051 let output = run_output(
11052 r#"
11053let x = 5
11054let result = case {
11055 x > 10 => "big",
11056 x > 0 => "positive",
11057 _ => "other"
11058}
11059print(result)
11060"#,
11061 );
11062 assert_eq!(output, vec!["positive"]);
11063 }
11064
11065 #[test]
11066 fn test_vm_parallel_map() {
11067 let result = run("map(range(15000), (x) => x * 2)").unwrap();
11069 if let VmValue::List(items) = result {
11070 assert_eq!(items.len(), 15000);
11071 assert!(matches!(items[0], VmValue::Int(0)));
11072 assert!(matches!(items[1], VmValue::Int(2)));
11073 assert!(matches!(items[14999], VmValue::Int(29998)));
11074 } else {
11075 panic!("Expected list, got {:?}", result);
11076 }
11077 }
11078
11079 #[test]
11080 fn test_vm_parallel_filter() {
11081 let result = run("filter(range(20000), (x) => x % 2 == 0)").unwrap();
11082 if let VmValue::List(items) = result {
11083 assert_eq!(items.len(), 10000);
11084 assert!(matches!(items[0], VmValue::Int(0)));
11085 assert!(matches!(items[1], VmValue::Int(2)));
11086 } else {
11087 panic!("Expected list, got {:?}", result);
11088 }
11089 }
11090
11091 #[test]
11092 fn test_vm_parallel_sum() {
11093 let result = run("sum(range(20000))").unwrap();
11094 assert!(matches!(result, VmValue::Int(199990000)));
11096 }
11097
11098 #[test]
11099 fn test_vm_recursive_fib() {
11100 let output = run_output(
11101 "fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\nprint(fib(10))",
11102 );
11103 assert_eq!(output, vec!["55"]);
11104 }
11105
11106 #[test]
11107 fn test_vm_if_else_expr() {
11108 let output = run_output(
11110 "fn abs(n) { if n < 0 { 0 - n } else { n } }\nprint(abs(-5))\nprint(abs(3))",
11111 );
11112 assert_eq!(output, vec!["5", "3"]);
11113 }
11114
11115 #[test]
11118 fn test_vm_struct_creation() {
11119 let output = run_output(
11120 "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.0, y: 2.0 }\nprint(p.x)\nprint(p.y)",
11121 );
11122 assert_eq!(output, vec!["1.0", "2.0"]);
11123 }
11124
11125 #[test]
11126 fn test_vm_struct_nested() {
11127 let output = run_output(
11128 "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)",
11129 );
11130 assert_eq!(output, vec!["0.0"]);
11131 }
11132
11133 #[test]
11134 fn test_vm_enum_creation() {
11135 let output = run_output("enum Color { Red, Green, Blue }\nlet c = Color::Red\nprint(c)");
11136 assert_eq!(output, vec!["Color::Red"]);
11137 }
11138
11139 #[test]
11140 fn test_vm_enum_with_fields() {
11141 let output = run_output(
11142 "enum Shape { Circle(float64), Rect(float64, float64) }\nlet s = Shape::Circle(5.0)\nprint(s)",
11143 );
11144 assert!(output[0].contains("Circle"));
11145 }
11146
11147 #[test]
11148 fn test_vm_impl_method() {
11149 let output = run_output(
11150 "struct Counter { value: int64 }\nimpl Counter {\n fn get(self) { self.value }\n}\nlet c = Counter { value: 42 }\nprint(c.get())",
11151 );
11152 assert_eq!(output, vec!["42"]);
11153 }
11154
11155 #[test]
11156 fn test_vm_try_catch_throw() {
11157 let output = run_output("try {\n throw \"oops\"\n} catch e {\n print(e)\n}");
11158 assert_eq!(output, vec!["oops"]);
11159 }
11160
11161 #[test]
11162 fn test_vm_string_split() {
11163 let output = run_output("let parts = \"hello world\".split(\" \")\nprint(parts)");
11164 assert_eq!(output, vec!["[hello, world]"]);
11165 }
11166
11167 #[test]
11168 fn test_vm_string_trim() {
11169 let output = run_output("print(\" hello \".trim())");
11170 assert_eq!(output, vec!["hello"]);
11171 }
11172
11173 #[test]
11174 fn test_vm_string_contains() {
11175 let output = run_output("print(\"hello world\".contains(\"world\"))");
11176 assert_eq!(output, vec!["true"]);
11177 }
11178
11179 #[test]
11180 fn test_vm_string_upper_lower() {
11181 let output = run_output("print(\"hello\".to_upper())\nprint(\"HELLO\".to_lower())");
11182 assert_eq!(output, vec!["HELLO", "hello"]);
11183 }
11184
11185 #[test]
11186 fn test_vm_math_sqrt() {
11187 let output = run_output("print(sqrt(16.0))");
11188 assert_eq!(output, vec!["4.0"]);
11189 }
11190
11191 #[test]
11192 fn test_vm_math_floor_ceil() {
11193 let output = run_output("print(floor(3.7))\nprint(ceil(3.2))");
11194 assert_eq!(output, vec!["3.0", "4.0"]);
11195 }
11196
11197 #[test]
11198 fn test_vm_math_trig() {
11199 let output = run_output("print(sin(0.0))\nprint(cos(0.0))");
11200 assert_eq!(output, vec!["0.0", "1.0"]);
11201 }
11202
11203 #[test]
11204 fn test_vm_assert_pass() {
11205 run("assert(true)").unwrap();
11206 run("assert_eq(1 + 1, 2)").unwrap();
11207 }
11208
11209 #[test]
11210 fn test_vm_assert_fail() {
11211 assert!(run("assert(false)").is_err());
11212 assert!(run("assert_eq(1, 2)").is_err());
11213 }
11214
11215 #[test]
11216 fn test_vm_join() {
11217 let output = run_output("print(join(\", \", [\"a\", \"b\", \"c\"]))");
11218 assert_eq!(output, vec!["a, b, c"]);
11219 }
11220
11221 #[test]
11222 fn test_vm_list_method_len() {
11223 let output = run_output("print([1, 2, 3].len())");
11224 assert_eq!(output, vec!["3"]);
11225 }
11226
11227 #[test]
11228 fn test_vm_list_method_map() {
11229 let output = run_output("print([1, 2, 3].map((x) => x * 2))");
11230 assert_eq!(output, vec!["[2, 4, 6]"]);
11231 }
11232
11233 #[test]
11234 fn test_vm_list_method_filter() {
11235 let output = run_output("print([1, 2, 3, 4, 5].filter((x) => x > 3))");
11236 assert_eq!(output, vec!["[4, 5]"]);
11237 }
11238
11239 #[test]
11240 fn test_vm_string_replace() {
11241 let output = run_output("print(\"hello world\".replace(\"world\", \"rust\"))");
11242 assert_eq!(output, vec!["hello rust"]);
11243 }
11244
11245 #[test]
11246 fn test_vm_string_starts_ends() {
11247 let output = run_output(
11248 "print(\"hello\".starts_with(\"hel\"))\nprint(\"hello\".ends_with(\"llo\"))",
11249 );
11250 assert_eq!(output, vec!["true", "true"]);
11251 }
11252
11253 #[test]
11254 fn test_vm_math_log() {
11255 let result = run("log(1.0)").unwrap();
11256 if let VmValue::Float(f) = result {
11257 assert!((f - 0.0).abs() < 1e-10);
11258 } else {
11259 panic!("Expected float");
11260 }
11261 }
11262
11263 #[test]
11264 fn test_vm_pow_builtin() {
11265 let output = run_output("print(pow(2.0, 10.0))");
11266 assert_eq!(output, vec!["1024.0"]);
11267 }
11268
11269 #[test]
11270 fn test_vm_round_builtin() {
11271 let output = run_output("print(round(3.5))");
11272 assert_eq!(output, vec!["4.0"]);
11273 }
11274
11275 #[test]
11276 fn test_vm_try_catch_runtime_error() {
11277 let output = run_output("try {\n let x = 1 / 0\n} catch e {\n print(e)\n}");
11278 assert_eq!(output, vec!["Division by zero"]);
11279 }
11280
11281 #[test]
11282 fn test_vm_struct_field_access() {
11283 let output = run_output(
11284 "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.5, y: 2.5 }\nprint(p.x)",
11285 );
11286 assert_eq!(output, vec!["1.5"]);
11287 }
11288
11289 #[test]
11290 fn test_vm_enum_match() {
11291 let output = run_output(
11292 "enum Dir { North, South }\nlet d = Dir::North\nmatch d { Dir::North => print(\"north\"), _ => print(\"other\") }",
11293 );
11294 assert!(!output.is_empty());
11296 }
11297
11298 #[test]
11299 fn test_vm_impl_method_with_args() {
11300 let output = run_output(
11301 "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())",
11302 );
11303 assert_eq!(output, vec!["12.0"]);
11304 }
11305
11306 #[test]
11307 fn test_vm_string_len() {
11308 let output = run_output("print(\"hello\".len())");
11309 assert_eq!(output, vec!["5"]);
11310 }
11311
11312 #[test]
11313 fn test_vm_list_reduce() {
11314 let output = run_output(
11315 "let nums = [1, 2, 3, 4]\nlet s = nums.reduce(0, (acc, x) => acc + x)\nprint(s)",
11316 );
11317 assert_eq!(output, vec!["10"]);
11318 }
11319
11320 #[test]
11321 fn test_vm_nested_try_catch() {
11322 let output = run_output(
11323 "try {\n try {\n throw \"inner\"\n } catch e {\n print(e)\n throw \"outer\"\n }\n} catch e2 {\n print(e2)\n}",
11324 );
11325 assert_eq!(output, vec!["inner", "outer"]);
11326 }
11327
11328 #[test]
11329 fn test_vm_math_pow() {
11330 let output = run_output("print(pow(2.0, 10.0))");
11331 assert_eq!(output, vec!["1024.0"]);
11332 }
11333
11334 #[test]
11337 fn test_vm_json_parse() {
11338 let output = run_output(
11339 r#"let m = map_from("a", 1, "b", "hello")
11340let s = json_stringify(m)
11341let m2 = json_parse(s)
11342print(m2["a"])
11343print(m2["b"])"#,
11344 );
11345 assert_eq!(output, vec!["1", "hello"]);
11346 }
11347
11348 #[test]
11349 fn test_vm_json_stringify() {
11350 let output = run_output(
11351 r#"let m = map_from("x", 1, "y", 2)
11352let s = json_stringify(m)
11353print(s)"#,
11354 );
11355 assert_eq!(output, vec![r#"{"x":1,"y":2}"#]);
11356 }
11357
11358 #[test]
11359 fn test_vm_map_from_and_access() {
11360 let output = run_output(
11361 r#"let m = map_from("a", 10, "b", 20)
11362print(m["a"])
11363print(m.b)"#,
11364 );
11365 assert_eq!(output, vec!["10", "20"]);
11366 }
11367
11368 #[test]
11369 fn test_vm_map_methods() {
11370 let output = run_output(
11371 r#"let m = map_from("a", 1, "b", 2)
11372print(m.keys())
11373print(m.values())
11374print(m.contains_key("a"))
11375print(m.len())"#,
11376 );
11377 assert_eq!(output, vec!["[a, b]", "[1, 2]", "true", "2"]);
11378 }
11379
11380 #[test]
11381 fn test_vm_map_set_index() {
11382 let output = run_output(
11383 r#"let m = map_from("a", 1)
11384m["b"] = 2
11385print(m["b"])"#,
11386 );
11387 assert_eq!(output, vec!["2"]);
11388 }
11389
11390 #[test]
11391 fn test_vm_map_iteration() {
11392 let output = run_output(
11393 r#"let m = map_from("x", 10, "y", 20)
11394for kv in m {
11395 print(kv[0])
11396}"#,
11397 );
11398 assert_eq!(output, vec!["x", "y"]);
11399 }
11400
11401 #[test]
11402 fn test_vm_file_read_write() {
11403 let output = run_output(
11404 r#"write_file("/tmp/tl_vm_test.txt", "vm hello")
11405print(read_file("/tmp/tl_vm_test.txt"))
11406print(file_exists("/tmp/tl_vm_test.txt"))"#,
11407 );
11408 assert_eq!(output, vec!["vm hello", "true"]);
11409 }
11410
11411 #[test]
11412 fn test_vm_env_get_set() {
11413 let output = run_output(
11414 r#"env_set("TL_VM_TEST", "abc")
11415print(env_get("TL_VM_TEST"))"#,
11416 );
11417 assert_eq!(output, vec!["abc"]);
11418 }
11419
11420 #[test]
11421 fn test_vm_regex_match() {
11422 let output = run_output(
11423 r#"print(regex_match("\\d+", "abc123"))
11424print(regex_match("^\\d+$", "abc"))"#,
11425 );
11426 assert_eq!(output, vec!["true", "false"]);
11427 }
11428
11429 #[test]
11430 fn test_vm_regex_find() {
11431 let output = run_output(
11432 r#"let m = regex_find("\\d+", "abc123def456")
11433print(len(m))
11434print(m[0])"#,
11435 );
11436 assert_eq!(output, vec!["2", "123"]);
11437 }
11438
11439 #[test]
11440 fn test_vm_regex_replace() {
11441 let output = run_output(r#"print(regex_replace("\\d+", "abc123", "X"))"#);
11442 assert_eq!(output, vec!["abcX"]);
11443 }
11444
11445 #[test]
11446 fn test_vm_now() {
11447 let output = run_output("let t = now()\nprint(type_of(t))");
11449 assert_eq!(output, vec!["datetime"]);
11450 }
11451
11452 #[test]
11453 fn test_vm_date_format() {
11454 let output = run_output(r#"print(date_format(1704067200000, "%Y-%m-%d"))"#);
11455 assert_eq!(output, vec!["2024-01-01"]);
11456 }
11457
11458 #[test]
11459 fn test_vm_date_parse() {
11460 let output = run_output(r#"print(date_parse("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))"#);
11461 assert_eq!(output, vec!["2024-01-01 00:00:00"]);
11462 }
11463
11464 #[test]
11465 fn test_vm_string_chars() {
11466 let output = run_output(r#"print(len("hello".chars()))"#);
11467 assert_eq!(output, vec!["5"]);
11468 }
11469
11470 #[test]
11471 fn test_vm_string_repeat() {
11472 let output = run_output(r#"print("ab".repeat(3))"#);
11473 assert_eq!(output, vec!["ababab"]);
11474 }
11475
11476 #[test]
11477 fn test_vm_string_index_of() {
11478 let output = run_output(r#"print("hello world".index_of("world"))"#);
11479 assert_eq!(output, vec!["6"]);
11480 }
11481
11482 #[test]
11483 fn test_vm_string_substring() {
11484 let output = run_output(r#"print("hello world".substring(0, 5))"#);
11485 assert_eq!(output, vec!["hello"]);
11486 }
11487
11488 #[test]
11489 fn test_vm_string_pad() {
11490 let output = run_output(
11491 r#"print("42".pad_left(5, "0"))
11492print("hi".pad_right(5, "."))"#,
11493 );
11494 assert_eq!(output, vec!["00042", "hi..."]);
11495 }
11496
11497 #[test]
11498 fn test_vm_list_sort() {
11499 let output = run_output(r#"print([3, 1, 2].sort())"#);
11500 assert_eq!(output, vec!["[1, 2, 3]"]);
11501 }
11502
11503 #[test]
11504 fn test_vm_list_reverse() {
11505 let output = run_output(r#"print([1, 2, 3].reverse())"#);
11506 assert_eq!(output, vec!["[3, 2, 1]"]);
11507 }
11508
11509 #[test]
11510 fn test_vm_list_contains() {
11511 let output = run_output(
11512 r#"print([1, 2, 3].contains(2))
11513print([1, 2, 3].contains(5))"#,
11514 );
11515 assert_eq!(output, vec!["true", "false"]);
11516 }
11517
11518 #[test]
11519 fn test_vm_list_slice() {
11520 let output = run_output(r#"print([1, 2, 3, 4, 5].slice(1, 4))"#);
11521 assert_eq!(output, vec!["[2, 3, 4]"]);
11522 }
11523
11524 #[test]
11525 fn test_vm_zip() {
11526 let output = run_output(
11527 r#"let p = zip([1, 2], ["a", "b"])
11528print(p[0])"#,
11529 );
11530 assert_eq!(output, vec!["[1, a]"]);
11531 }
11532
11533 #[test]
11534 fn test_vm_enumerate() {
11535 let output = run_output(
11536 r#"let e = enumerate(["a", "b", "c"])
11537print(e[1])"#,
11538 );
11539 assert_eq!(output, vec!["[1, b]"]);
11540 }
11541
11542 #[test]
11543 fn test_vm_bool() {
11544 let output = run_output(
11545 r#"print(bool(1))
11546print(bool(0))
11547print(bool(""))"#,
11548 );
11549 assert_eq!(output, vec!["true", "false", "false"]);
11550 }
11551
11552 #[test]
11553 fn test_vm_range_step() {
11554 let output = run_output(r#"print(range(0, 10, 3))"#);
11555 assert_eq!(output, vec!["[0, 3, 6, 9]"]);
11556 }
11557
11558 #[test]
11559 fn test_vm_int_bool() {
11560 let output = run_output(
11561 r#"print(int(true))
11562print(int(false))"#,
11563 );
11564 assert_eq!(output, vec!["1", "0"]);
11565 }
11566
11567 #[test]
11568 fn test_vm_map_len_typeof() {
11569 let output = run_output(
11570 r#"let m = map_from("a", 1)
11571print(len(m))
11572print(type_of(m))"#,
11573 );
11574 assert_eq!(output, vec!["1", "map"]);
11575 }
11576
11577 #[test]
11578 fn test_vm_json_file_roundtrip() {
11579 let output = run_output(
11580 r#"let data = map_from("name", "vm_test", "count", 99)
11581write_file("/tmp/tl_vm_json.json", json_stringify(data))
11582let parsed = json_parse(read_file("/tmp/tl_vm_json.json"))
11583print(parsed["name"])
11584print(parsed["count"])"#,
11585 );
11586 assert_eq!(output, vec!["vm_test", "99"]);
11587 }
11588
11589 #[test]
11592 fn test_vm_spawn_await_basic() {
11593 let output = run_output(
11594 r#"fn worker() { 42 }
11595let t = spawn(worker)
11596let result = await t
11597print(result)"#,
11598 );
11599 assert_eq!(output, vec!["42"]);
11600 }
11601
11602 #[test]
11603 fn test_vm_spawn_closure_with_capture() {
11604 let output = run_output(
11605 r#"let x = 10
11606let f = () => x + 5
11607let t = spawn(f)
11608print(await t)"#,
11609 );
11610 assert_eq!(output, vec!["15"]);
11611 }
11612
11613 #[test]
11614 fn test_vm_sleep() {
11615 let output = run_output(
11616 r#"sleep(10)
11617print("done")"#,
11618 );
11619 assert_eq!(output, vec!["done"]);
11620 }
11621
11622 #[test]
11623 fn test_vm_await_non_task_passthrough() {
11624 let output = run_output(r#"print(await 42)"#);
11625 assert_eq!(output, vec!["42"]);
11626 }
11627
11628 #[test]
11629 fn test_vm_spawn_multiple_await() {
11630 let output = run_output(
11631 r#"fn w1() { 1 }
11632fn w2() { 2 }
11633fn w3() { 3 }
11634let t1 = spawn(w1)
11635let t2 = spawn(w2)
11636let t3 = spawn(w3)
11637let a = await t1
11638let b = await t2
11639let c = await t3
11640print(a + b + c)"#,
11641 );
11642 assert_eq!(output, vec!["6"]);
11643 }
11644
11645 #[test]
11646 fn test_vm_channel_basic() {
11647 let output = run_output(
11648 r#"let ch = channel()
11649send(ch, 42)
11650let val = recv(ch)
11651print(val)"#,
11652 );
11653 assert_eq!(output, vec!["42"]);
11654 }
11655
11656 #[test]
11657 fn test_vm_channel_between_tasks() {
11658 let output = run_output(
11659 r#"let ch = channel()
11660fn producer() { send(ch, 100) }
11661let t = spawn(producer)
11662let val = recv(ch)
11663await t
11664print(val)"#,
11665 );
11666 assert_eq!(output, vec!["100"]);
11667 }
11668
11669 #[test]
11670 fn test_vm_try_recv_empty() {
11671 let output = run_output(
11672 r#"let ch = channel()
11673let val = try_recv(ch)
11674print(val)"#,
11675 );
11676 assert_eq!(output, vec!["none"]);
11677 }
11678
11679 #[test]
11680 fn test_vm_channel_multiple_values() {
11681 let output = run_output(
11682 r#"let ch = channel()
11683send(ch, 1)
11684send(ch, 2)
11685send(ch, 3)
11686print(recv(ch))
11687print(recv(ch))
11688print(recv(ch))"#,
11689 );
11690 assert_eq!(output, vec!["1", "2", "3"]);
11691 }
11692
11693 #[test]
11694 fn test_vm_channel_producer_consumer() {
11695 let output = run_output(
11696 r#"let ch = channel()
11697fn producer() {
11698 send(ch, 10)
11699 send(ch, 20)
11700 send(ch, 30)
11701}
11702let t = spawn(producer)
11703let a = recv(ch)
11704let b = recv(ch)
11705let c = recv(ch)
11706await t
11707print(a + b + c)"#,
11708 );
11709 assert_eq!(output, vec!["60"]);
11710 }
11711
11712 #[test]
11713 fn test_vm_await_all() {
11714 let output = run_output(
11715 r#"fn w1() { 10 }
11716fn w2() { 20 }
11717fn w3() { 30 }
11718let t1 = spawn(w1)
11719let t2 = spawn(w2)
11720let t3 = spawn(w3)
11721let results = await_all([t1, t2, t3])
11722print(sum(results))"#,
11723 );
11724 assert_eq!(output, vec!["60"]);
11725 }
11726
11727 #[test]
11728 fn test_vm_pmap_basic() {
11729 let output = run_output(
11730 r#"let results = pmap([1, 2, 3], (x) => x * 2)
11731print(results)"#,
11732 );
11733 assert_eq!(output, vec!["[2, 4, 6]"]);
11734 }
11735
11736 #[test]
11737 fn test_vm_pmap_order_preserved() {
11738 let output = run_output(
11739 r#"let results = pmap([10, 20, 30], (x) => x + 1)
11740print(results)"#,
11741 );
11742 assert_eq!(output, vec!["[11, 21, 31]"]);
11743 }
11744
11745 #[test]
11746 fn test_vm_timeout_success() {
11747 let output = run_output(
11748 r#"fn worker() { 42 }
11749let t = spawn(worker)
11750let result = timeout(t, 5000)
11751print(result)"#,
11752 );
11753 assert_eq!(output, vec!["42"]);
11754 }
11755
11756 #[test]
11757 fn test_vm_timeout_failure() {
11758 let output = run_output(
11759 r#"fn slow() { sleep(10000) }
11760let t = spawn(slow)
11761let result = "ok"
11762try {
11763 result = timeout(t, 50)
11764} catch e {
11765 result = e
11766}
11767print(result)"#,
11768 );
11769 assert_eq!(output, vec!["Task timed out"]);
11770 }
11771
11772 #[test]
11773 fn test_vm_spawn_error_propagation() {
11774 let output = run_output(
11775 r#"fn bad() { throw "bad thing" }
11776let result = "ok"
11777try {
11778 let t = spawn(bad)
11779 result = await t
11780} catch e {
11781 result = e
11782}
11783print(result)"#,
11784 );
11785 assert_eq!(output, vec!["bad thing"]);
11786 }
11787
11788 #[test]
11789 fn test_vm_spawn_producer_consumer_pipeline() {
11790 let output = run_output(
11791 r#"let ch = channel()
11792fn producer() {
11793 let mut i = 0
11794 while i < 5 {
11795 send(ch, i * 10)
11796 i = i + 1
11797 }
11798}
11799let t = spawn(producer)
11800let mut total = 0
11801let mut count = 0
11802while count < 5 {
11803 total = total + recv(ch)
11804 count = count + 1
11805}
11806await t
11807print(total)"#,
11808 );
11809 assert_eq!(output, vec!["100"]);
11810 }
11811
11812 #[test]
11813 fn test_vm_type_of_task_channel() {
11814 let output = run_output(
11815 r#"fn worker() { 1 }
11816let t = spawn(worker)
11817let ch = channel()
11818print(type_of(t))
11819print(type_of(ch))
11820await t"#,
11821 );
11822 assert_eq!(output, vec!["task", "channel"]);
11823 }
11824
11825 #[test]
11828 fn test_vm_basic_generator() {
11829 let output = run_output(
11830 r#"fn gen() {
11831 yield 1
11832 yield 2
11833 yield 3
11834}
11835let g = gen()
11836print(next(g))
11837print(next(g))
11838print(next(g))
11839print(next(g))"#,
11840 );
11841 assert_eq!(output, vec!["1", "2", "3", "none"]);
11842 }
11843
11844 #[test]
11845 fn test_vm_generator_exhaustion() {
11846 let output = run_output(
11847 r#"fn gen() {
11848 yield 42
11849}
11850let g = gen()
11851print(next(g))
11852print(next(g))
11853print(next(g))"#,
11854 );
11855 assert_eq!(output, vec!["42", "none", "none"]);
11856 }
11857
11858 #[test]
11859 fn test_vm_generator_with_loop() {
11860 let output = run_output(
11861 r#"fn counter() {
11862 let mut i = 0
11863 while i < 3 {
11864 yield i
11865 i = i + 1
11866 }
11867}
11868let g = counter()
11869print(next(g))
11870print(next(g))
11871print(next(g))
11872print(next(g))"#,
11873 );
11874 assert_eq!(output, vec!["0", "1", "2", "none"]);
11875 }
11876
11877 #[test]
11878 fn test_vm_generator_with_args() {
11879 let output = run_output(
11880 r#"fn count_from(start) {
11881 let mut i = start
11882 while i < start + 3 {
11883 yield i
11884 i = i + 1
11885 }
11886}
11887let g = count_from(10)
11888print(next(g))
11889print(next(g))
11890print(next(g))
11891print(next(g))"#,
11892 );
11893 assert_eq!(output, vec!["10", "11", "12", "none"]);
11894 }
11895
11896 #[test]
11897 fn test_vm_generator_yield_none() {
11898 let output = run_output(
11899 r#"fn gen() {
11900 yield
11901 yield 5
11902}
11903let g = gen()
11904print(next(g))
11905print(next(g))
11906print(next(g))"#,
11907 );
11908 assert_eq!(output, vec!["none", "5", "none"]);
11909 }
11910
11911 #[test]
11912 fn test_vm_is_generator() {
11913 let output = run_output(
11914 r#"fn gen() { yield 1 }
11915let g = gen()
11916print(is_generator(g))
11917print(is_generator(42))
11918print(is_generator(none))"#,
11919 );
11920 assert_eq!(output, vec!["true", "false", "false"]);
11921 }
11922
11923 #[test]
11924 fn test_vm_multiple_generators() {
11925 let output = run_output(
11926 r#"fn gen() {
11927 yield 1
11928 yield 2
11929}
11930let g1 = gen()
11931let g2 = gen()
11932print(next(g1))
11933print(next(g2))
11934print(next(g1))
11935print(next(g2))"#,
11936 );
11937 assert_eq!(output, vec!["1", "1", "2", "2"]);
11938 }
11939
11940 #[test]
11941 fn test_vm_for_over_generator() {
11942 let output = run_output(
11943 r#"fn gen() {
11944 yield 10
11945 yield 20
11946 yield 30
11947}
11948for x in gen() {
11949 print(x)
11950}"#,
11951 );
11952 assert_eq!(output, vec!["10", "20", "30"]);
11953 }
11954
11955 #[test]
11956 fn test_vm_iter_builtin() {
11957 let output = run_output(
11958 r#"let g = iter([1, 2, 3])
11959print(next(g))
11960print(next(g))
11961print(next(g))
11962print(next(g))"#,
11963 );
11964 assert_eq!(output, vec!["1", "2", "3", "none"]);
11965 }
11966
11967 #[test]
11968 fn test_vm_take_builtin() {
11969 let output = run_output(
11970 r#"fn naturals() {
11971 let mut n = 0
11972 while true {
11973 yield n
11974 n = n + 1
11975 }
11976}
11977let g = take(naturals(), 5)
11978print(next(g))
11979print(next(g))
11980print(next(g))
11981print(next(g))
11982print(next(g))
11983print(next(g))"#,
11984 );
11985 assert_eq!(output, vec!["0", "1", "2", "3", "4", "none"]);
11986 }
11987
11988 #[test]
11989 fn test_vm_skip_builtin() {
11990 let output = run_output(
11991 r#"let g = skip(iter([10, 20, 30, 40, 50]), 2)
11992print(next(g))
11993print(next(g))
11994print(next(g))
11995print(next(g))"#,
11996 );
11997 assert_eq!(output, vec!["30", "40", "50", "none"]);
11998 }
11999
12000 #[test]
12001 fn test_vm_gen_collect() {
12002 let output = run_output(
12003 r#"fn gen() {
12004 yield 1
12005 yield 2
12006 yield 3
12007}
12008let result = gen_collect(gen())
12009print(result)"#,
12010 );
12011 assert_eq!(output, vec!["[1, 2, 3]"]);
12012 }
12013
12014 #[test]
12015 fn test_vm_gen_map() {
12016 let output = run_output(
12017 r#"let g = gen_map(iter([1, 2, 3]), (x) => x * 10)
12018print(gen_collect(g))"#,
12019 );
12020 assert_eq!(output, vec!["[10, 20, 30]"]);
12021 }
12022
12023 #[test]
12024 fn test_vm_gen_filter() {
12025 let output = run_output(
12026 r#"let g = gen_filter(iter([1, 2, 3, 4, 5, 6]), (x) => x % 2 == 0)
12027print(gen_collect(g))"#,
12028 );
12029 assert_eq!(output, vec!["[2, 4, 6]"]);
12030 }
12031
12032 #[test]
12033 fn test_vm_chain() {
12034 let output = run_output(
12035 r#"let g = chain(iter([1, 2]), iter([3, 4]))
12036print(gen_collect(g))"#,
12037 );
12038 assert_eq!(output, vec!["[1, 2, 3, 4]"]);
12039 }
12040
12041 #[test]
12042 fn test_vm_gen_zip() {
12043 let output = run_output(
12044 r#"let g = gen_zip(iter([1, 2, 3]), iter([10, 20, 30]))
12045print(gen_collect(g))"#,
12046 );
12047 assert_eq!(output, vec!["[[1, 10], [2, 20], [3, 30]]"]);
12048 }
12049
12050 #[test]
12051 fn test_vm_gen_enumerate() {
12052 let output = run_output(
12053 r#"let g = gen_enumerate(iter([10, 20, 30]))
12054print(gen_collect(g))"#,
12055 );
12056 assert_eq!(output, vec!["[[0, 10], [1, 20], [2, 30]]"]);
12057 }
12058
12059 #[test]
12060 fn test_vm_combinator_chaining() {
12061 let output = run_output(
12062 r#"fn naturals() {
12063 let mut n = 0
12064 while true {
12065 yield n
12066 n = n + 1
12067 }
12068}
12069let result = gen_collect(gen_map(gen_filter(take(naturals(), 10), (x) => x % 2 == 0), (x) => x * x))
12070print(result)"#,
12071 );
12072 assert_eq!(output, vec!["[0, 4, 16, 36, 64]"]);
12073 }
12074
12075 #[test]
12076 fn test_vm_for_over_take() {
12077 let output = run_output(
12078 r#"fn naturals() {
12079 let mut n = 0
12080 while true {
12081 yield n
12082 n = n + 1
12083 }
12084}
12085for x in take(naturals(), 5) {
12086 print(x)
12087}"#,
12088 );
12089 assert_eq!(output, vec!["0", "1", "2", "3", "4"]);
12090 }
12091
12092 #[test]
12093 fn test_vm_generator_error_propagation() {
12094 let result = run(r#"fn bad_gen() {
12095 yield 1
12096 throw "oops"
12097}
12098let g = bad_gen()
12099let mut caught = ""
12100next(g)
12101try {
12102 next(g)
12103} catch e {
12104 caught = e
12105}
12106print(caught)"#);
12107 assert!(result.is_ok());
12109 }
12110
12111 #[test]
12112 fn test_vm_fibonacci_generator() {
12113 let output = run_output(
12114 r#"fn fib() {
12115 let mut a = 0
12116 let mut b = 1
12117 while true {
12118 yield a
12119 let temp = a + b
12120 a = b
12121 b = temp
12122 }
12123}
12124print(gen_collect(take(fib(), 8)))"#,
12125 );
12126 assert_eq!(output, vec!["[0, 1, 1, 2, 3, 5, 8, 13]"]);
12127 }
12128
12129 #[test]
12130 fn test_vm_generator_method_syntax() {
12131 let output = run_output(
12132 r#"fn gen() {
12133 yield 1
12134 yield 2
12135 yield 3
12136}
12137let g = gen()
12138print(type_of(g))"#,
12139 );
12140 assert_eq!(output, vec!["generator"]);
12141 }
12142
12143 #[test]
12146 fn test_vm_ok_err_builtins() {
12147 let output = run_output("let r = Ok(42)\nprint(r)");
12148 assert_eq!(output, vec!["Result::Ok(42)"]);
12149
12150 let output = run_output("let r = Err(\"fail\")\nprint(r)");
12151 assert_eq!(output, vec!["Result::Err(fail)"]);
12152 }
12153
12154 #[test]
12155 fn test_vm_is_ok_is_err() {
12156 let output = run_output("print(is_ok(Ok(42)))");
12157 assert_eq!(output, vec!["true"]);
12158 let output = run_output("print(is_err(Ok(42)))");
12159 assert_eq!(output, vec!["false"]);
12160 let output = run_output("print(is_ok(Err(\"fail\")))");
12161 assert_eq!(output, vec!["false"]);
12162 let output = run_output("print(is_err(Err(\"fail\")))");
12163 assert_eq!(output, vec!["true"]);
12164 }
12165
12166 #[test]
12167 fn test_vm_unwrap_ok() {
12168 let output = run_output("print(unwrap(Ok(42)))");
12169 assert_eq!(output, vec!["42"]);
12170 }
12171
12172 #[test]
12173 fn test_vm_unwrap_err_panics() {
12174 let result = run("unwrap(Err(\"fail\"))");
12175 assert!(result.is_err());
12176 }
12177
12178 #[test]
12179 fn test_vm_try_on_ok() {
12180 let output = run_output(
12181 r#"fn get_val() { Ok(42) }
12182fn process() { let v = get_val()? + 1
12183Ok(v) }
12184print(process())"#,
12185 );
12186 assert_eq!(output, vec!["Result::Ok(43)"]);
12187 }
12188
12189 #[test]
12190 fn test_vm_try_on_err_propagates() {
12191 let output = run_output(
12192 r#"fn failing() { Err("oops") }
12193fn process() { let v = failing()?
12194Ok(v) }
12195print(process())"#,
12196 );
12197 assert_eq!(output, vec!["Result::Err(oops)"]);
12198 }
12199
12200 #[test]
12201 fn test_vm_try_on_none_propagates() {
12202 let output = run_output(
12203 r#"fn get_none() { none }
12204fn process() { let v = get_none()?
1220542 }
12206print(process())"#,
12207 );
12208 assert_eq!(output, vec!["none"]);
12209 }
12210
12211 #[test]
12212 fn test_vm_try_passthrough() {
12213 let output = run_output(
12215 r#"fn get_val() { 42 }
12216fn process() { let v = get_val()?
12217v + 1 }
12218print(process())"#,
12219 );
12220 assert_eq!(output, vec!["43"]);
12221 }
12222
12223 #[test]
12224 fn test_vm_result_match() {
12225 let output = run_output(
12226 r#"let r = Ok(42)
12227print(is_ok(r))
12228print(unwrap(r))"#,
12229 );
12230 assert_eq!(output, vec!["true", "42"]);
12231 }
12232
12233 #[test]
12234 fn test_vm_result_match_err() {
12235 let output = run_output(
12236 r#"let r = Err("fail")
12237print(is_err(r))
12238match r {
12239 Result::Err(e) => print("got error"),
12240 _ => print("no error")
12241}"#,
12242 );
12243 assert_eq!(output, vec!["true", "got error"]);
12244 }
12245
12246 #[test]
12249 fn test_vm_set_from_dedup() {
12250 let output = run_output(
12251 r#"let s = set_from([1, 2, 3, 2, 1])
12252print(len(s))
12253print(type_of(s))"#,
12254 );
12255 assert_eq!(output, vec!["3", "set"]);
12256 }
12257
12258 #[test]
12259 fn test_vm_set_add() {
12260 let output = run_output(
12261 r#"let s = set_from([1, 2])
12262let s2 = set_add(s, 3)
12263let s3 = set_add(s2, 2)
12264print(len(s2))
12265print(len(s3))"#,
12266 );
12267 assert_eq!(output, vec!["3", "3"]);
12268 }
12269
12270 #[test]
12271 fn test_vm_set_remove() {
12272 let output = run_output(
12273 r#"let s = set_from([1, 2, 3])
12274let s2 = set_remove(s, 2)
12275print(len(s2))
12276print(set_contains(s2, 2))"#,
12277 );
12278 assert_eq!(output, vec!["2", "false"]);
12279 }
12280
12281 #[test]
12282 fn test_vm_set_contains() {
12283 let output = run_output(
12284 r#"let s = set_from([1, 2, 3])
12285print(set_contains(s, 2))
12286print(set_contains(s, 5))"#,
12287 );
12288 assert_eq!(output, vec!["true", "false"]);
12289 }
12290
12291 #[test]
12292 fn test_vm_set_union() {
12293 let output = run_output(
12294 r#"let a = set_from([1, 2, 3])
12295let b = set_from([3, 4, 5])
12296let c = set_union(a, b)
12297print(len(c))"#,
12298 );
12299 assert_eq!(output, vec!["5"]);
12300 }
12301
12302 #[test]
12303 fn test_vm_set_intersection() {
12304 let output = run_output(
12305 r#"let a = set_from([1, 2, 3])
12306let b = set_from([2, 3, 4])
12307let c = set_intersection(a, b)
12308print(len(c))"#,
12309 );
12310 assert_eq!(output, vec!["2"]);
12311 }
12312
12313 #[test]
12314 fn test_vm_set_difference() {
12315 let output = run_output(
12316 r#"let a = set_from([1, 2, 3])
12317let b = set_from([2, 3, 4])
12318let c = set_difference(a, b)
12319print(len(c))"#,
12320 );
12321 assert_eq!(output, vec!["1"]);
12322 }
12323
12324 #[test]
12325 fn test_vm_set_for_loop() {
12326 let output = run_output(
12327 r#"let s = set_from([10, 20, 30])
12328let total = 0
12329for item in s {
12330 total = total + item
12331}
12332print(total)"#,
12333 );
12334 assert_eq!(output, vec!["60"]);
12335 }
12336
12337 #[test]
12338 fn test_vm_set_to_list() {
12339 let output = run_output(
12340 r#"let s = set_from([3, 1, 2])
12341let lst = s.to_list()
12342print(type_of(lst))
12343print(len(lst))"#,
12344 );
12345 assert_eq!(output, vec!["list", "3"]);
12346 }
12347
12348 #[test]
12349 fn test_vm_set_method_contains() {
12350 let output = run_output(
12351 r#"let s = set_from([1, 2, 3])
12352print(s.contains(2))
12353print(s.contains(5))"#,
12354 );
12355 assert_eq!(output, vec!["true", "false"]);
12356 }
12357
12358 #[test]
12359 fn test_vm_set_method_add_remove() {
12360 let output = run_output(
12361 r#"let s = set_from([1, 2])
12362let s2 = s.add(3)
12363print(s2.len())
12364let s3 = s2.remove(1)
12365print(s3.len())"#,
12366 );
12367 assert_eq!(output, vec!["3", "2"]);
12368 }
12369
12370 #[test]
12371 fn test_vm_set_method_union_intersection_difference() {
12372 let output = run_output(
12373 r#"let a = set_from([1, 2, 3])
12374let b = set_from([2, 3, 4])
12375print(a.union(b).len())
12376print(a.intersection(b).len())
12377print(a.difference(b).len())"#,
12378 );
12379 assert_eq!(output, vec!["4", "2", "1"]);
12380 }
12381
12382 #[test]
12383 fn test_vm_set_empty() {
12384 let output = run_output(
12385 r#"let s = set_from([])
12386print(len(s))
12387let s2 = s.add(1)
12388print(len(s2))"#,
12389 );
12390 assert_eq!(output, vec!["0", "1"]);
12391 }
12392
12393 #[test]
12394 fn test_vm_set_string_values() {
12395 let output = run_output(
12396 r#"let s = set_from(["a", "b", "a", "c"])
12397print(len(s))
12398print(s.contains("b"))"#,
12399 );
12400 assert_eq!(output, vec!["3", "true"]);
12401 }
12402
12403 #[test]
12406 fn test_vm_import_with_caching() {
12407 let vm = Vm::new();
12409 assert!(vm.module_cache.is_empty());
12410 assert!(vm.importing_files.is_empty());
12411 assert!(vm.file_path.is_none());
12412 }
12413
12414 #[test]
12415 fn test_vm_use_single_file() {
12416 let dir = tempfile::tempdir().unwrap();
12418 let lib_path = dir.path().join("math.tl");
12419 std::fs::write(&lib_path, "let PI = 3.14\nfn add(a, b) { a + b }").unwrap();
12420
12421 let main_path = dir.path().join("main.tl");
12422 std::fs::write(&main_path, "use math\nprint(add(1, 2))").unwrap();
12423
12424 let source = std::fs::read_to_string(&main_path).unwrap();
12425 let program = tl_parser::parse(&source).unwrap();
12426 let proto = crate::compiler::compile(&program).unwrap();
12427
12428 let mut vm = Vm::new();
12429 vm.file_path = Some(main_path.to_string_lossy().to_string());
12430 vm.execute(&proto).unwrap();
12431 assert_eq!(vm.output, vec!["3"]);
12432 }
12433
12434 #[test]
12435 fn test_vm_use_wildcard() {
12436 let dir = tempfile::tempdir().unwrap();
12437 std::fs::write(
12438 dir.path().join("helpers.tl"),
12439 "fn greet() { \"hello\" }\nfn farewell() { \"bye\" }",
12440 )
12441 .unwrap();
12442
12443 let main_src = "use helpers.*\nprint(greet())\nprint(farewell())";
12444 let main_path = dir.path().join("main.tl");
12445 std::fs::write(&main_path, main_src).unwrap();
12446
12447 let program = tl_parser::parse(main_src).unwrap();
12448 let proto = crate::compiler::compile(&program).unwrap();
12449
12450 let mut vm = Vm::new();
12451 vm.file_path = Some(main_path.to_string_lossy().to_string());
12452 vm.execute(&proto).unwrap();
12453 assert_eq!(vm.output, vec!["hello", "bye"]);
12454 }
12455
12456 #[test]
12457 fn test_vm_use_aliased() {
12458 let dir = tempfile::tempdir().unwrap();
12459 std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12460
12461 let main_src = "use mylib as m\nprint(m.compute())";
12462 let main_path = dir.path().join("main.tl");
12463 std::fs::write(&main_path, main_src).unwrap();
12464
12465 let program = tl_parser::parse(main_src).unwrap();
12466 let proto = crate::compiler::compile(&program).unwrap();
12467
12468 let mut vm = Vm::new();
12469 vm.file_path = Some(main_path.to_string_lossy().to_string());
12470 vm.execute(&proto).unwrap();
12471 assert_eq!(vm.output, vec!["42"]);
12472 }
12473
12474 #[test]
12475 fn test_vm_use_directory_module() {
12476 let dir = tempfile::tempdir().unwrap();
12477 std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12478 std::fs::write(dir.path().join("utils/mod.tl"), "fn helper() { 99 }").unwrap();
12479
12480 let main_src = "use utils\nprint(helper())";
12481 let main_path = dir.path().join("main.tl");
12482 std::fs::write(&main_path, main_src).unwrap();
12483
12484 let program = tl_parser::parse(main_src).unwrap();
12485 let proto = crate::compiler::compile(&program).unwrap();
12486
12487 let mut vm = Vm::new();
12488 vm.file_path = Some(main_path.to_string_lossy().to_string());
12489 vm.execute(&proto).unwrap();
12490 assert_eq!(vm.output, vec!["99"]);
12491 }
12492
12493 #[test]
12494 fn test_vm_circular_import_detection() {
12495 let dir = tempfile::tempdir().unwrap();
12496 let a_path = dir.path().join("a.tl");
12497 let b_path = dir.path().join("b.tl");
12498 std::fs::write(&a_path, &format!("import \"{}\"", b_path.to_string_lossy())).unwrap();
12499 std::fs::write(&b_path, &format!("import \"{}\"", a_path.to_string_lossy())).unwrap();
12500
12501 let source = std::fs::read_to_string(&a_path).unwrap();
12502 let program = tl_parser::parse(&source).unwrap();
12503 let proto = crate::compiler::compile(&program).unwrap();
12504
12505 let mut vm = Vm::new();
12506 vm.file_path = Some(a_path.to_string_lossy().to_string());
12507 let result = vm.execute(&proto);
12508 assert!(result.is_err());
12509 assert!(format!("{:?}", result).contains("Circular import"));
12510 }
12511
12512 #[test]
12513 fn test_vm_module_caching() {
12514 let dir = tempfile::tempdir().unwrap();
12516 std::fs::write(dir.path().join("cached.tl"), "let X = 42").unwrap();
12517
12518 let main_src = "use cached\nuse cached\nprint(X)";
12519 let main_path = dir.path().join("main.tl");
12520 std::fs::write(&main_path, main_src).unwrap();
12521
12522 let program = tl_parser::parse(main_src).unwrap();
12523 let proto = crate::compiler::compile(&program).unwrap();
12524
12525 let mut vm = Vm::new();
12526 vm.file_path = Some(main_path.to_string_lossy().to_string());
12527 vm.execute(&proto).unwrap();
12528 assert_eq!(vm.output, vec!["42"]);
12529 }
12530
12531 #[test]
12532 fn test_vm_existing_import_still_works() {
12533 let dir = tempfile::tempdir().unwrap();
12535 let lib_path = dir.path().join("lib.tl");
12536 std::fs::write(&lib_path, "fn imported_fn() { 123 }").unwrap();
12537
12538 let main_src = format!(
12539 "import \"{}\"\nprint(imported_fn())",
12540 lib_path.to_string_lossy()
12541 );
12542 let program = tl_parser::parse(&main_src).unwrap();
12543 let proto = crate::compiler::compile(&program).unwrap();
12544
12545 let mut vm = Vm::new();
12546 vm.execute(&proto).unwrap();
12547 assert_eq!(vm.output, vec!["123"]);
12548 }
12549
12550 #[test]
12551 fn test_vm_pub_fn_parsing() {
12552 let output = run_output("pub fn add(a, b) { a + b }\nprint(add(1, 2))");
12554 assert_eq!(output, vec!["3"]);
12555 }
12556
12557 #[test]
12558 fn test_vm_use_nested_path() {
12559 let dir = tempfile::tempdir().unwrap();
12560 std::fs::create_dir_all(dir.path().join("data")).unwrap();
12561 std::fs::write(
12562 dir.path().join("data/transforms.tl"),
12563 "fn clean(x) { x + 1 }",
12564 )
12565 .unwrap();
12566
12567 let main_src = "use data.transforms\nprint(clean(41))";
12568 let main_path = dir.path().join("main.tl");
12569 std::fs::write(&main_path, main_src).unwrap();
12570
12571 let program = tl_parser::parse(main_src).unwrap();
12572 let proto = crate::compiler::compile(&program).unwrap();
12573
12574 let mut vm = Vm::new();
12575 vm.file_path = Some(main_path.to_string_lossy().to_string());
12576 vm.execute(&proto).unwrap();
12577 assert_eq!(vm.output, vec!["42"]);
12578 }
12579
12580 #[test]
12583 fn test_integration_multi_file_use_functions() {
12584 let dir = tempfile::tempdir().unwrap();
12586 std::fs::write(
12587 dir.path().join("lib.tl"),
12588 "fn greet(name) { \"Hello, \" + name + \"!\" }\nfn double(x) { x * 2 }",
12589 )
12590 .unwrap();
12591
12592 let main_src = "use lib\nprint(greet(\"World\"))\nprint(double(21))";
12593 let main_path = dir.path().join("main.tl");
12594 std::fs::write(&main_path, main_src).unwrap();
12595
12596 let program = tl_parser::parse(main_src).unwrap();
12597 let proto = crate::compiler::compile(&program).unwrap();
12598 let mut vm = Vm::new();
12599 vm.file_path = Some(main_path.to_string_lossy().to_string());
12600 vm.execute(&proto).unwrap();
12601 assert_eq!(vm.output, vec!["Hello, World!", "42"]);
12602 }
12603
12604 #[test]
12605 fn test_integration_mixed_import_and_use() {
12606 let dir = tempfile::tempdir().unwrap();
12608 std::fs::write(dir.path().join("old_lib.tl"), "fn old_fn() { 10 }").unwrap();
12609 std::fs::write(dir.path().join("new_lib.tl"), "fn new_fn() { 20 }").unwrap();
12610
12611 let old_lib_abs = dir.path().join("old_lib.tl").to_string_lossy().to_string();
12612 let main_src = format!("import \"{old_lib_abs}\"\nuse new_lib\nprint(old_fn() + new_fn())");
12613 let main_path = dir.path().join("main.tl");
12614 std::fs::write(&main_path, &main_src).unwrap();
12615
12616 let program = tl_parser::parse(&main_src).unwrap();
12617 let proto = crate::compiler::compile(&program).unwrap();
12618 let mut vm = Vm::new();
12619 vm.file_path = Some(main_path.to_string_lossy().to_string());
12620 vm.execute(&proto).unwrap();
12621 assert_eq!(vm.output, vec!["30"]);
12622 }
12623
12624 #[test]
12625 fn test_integration_directory_module_with_mod_tl() {
12626 let dir = tempfile::tempdir().unwrap();
12628 std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12629 std::fs::write(
12630 dir.path().join("utils/mod.tl"),
12631 "fn helper() { 99 }\nfn format_num(n) { str(n) + \"!\" }",
12632 )
12633 .unwrap();
12634
12635 let main_src = "use utils\nprint(helper())\nprint(format_num(42))";
12636 let main_path = dir.path().join("main.tl");
12637 std::fs::write(&main_path, main_src).unwrap();
12638
12639 let program = tl_parser::parse(main_src).unwrap();
12640 let proto = crate::compiler::compile(&program).unwrap();
12641 let mut vm = Vm::new();
12642 vm.file_path = Some(main_path.to_string_lossy().to_string());
12643 vm.execute(&proto).unwrap();
12644 assert_eq!(vm.output, vec!["99", "42!"]);
12645 }
12646
12647 #[test]
12648 fn test_integration_circular_dep_error() {
12649 let dir = tempfile::tempdir().unwrap();
12650 let a_abs = dir.path().join("a.tl").to_string_lossy().to_string();
12651 let b_abs = dir.path().join("b.tl").to_string_lossy().to_string();
12652 std::fs::write(
12653 dir.path().join("a.tl"),
12654 format!("import \"{b_abs}\"\nfn fa() {{ 1 }}"),
12655 )
12656 .unwrap();
12657 std::fs::write(
12658 dir.path().join("b.tl"),
12659 format!("import \"{a_abs}\"\nfn fb() {{ 2 }}"),
12660 )
12661 .unwrap();
12662
12663 let main_src = format!("import \"{a_abs}\"");
12664 let program = tl_parser::parse(&main_src).unwrap();
12665 let proto = crate::compiler::compile(&program).unwrap();
12666 let mut vm = Vm::new();
12667 let result = vm.execute(&proto);
12668 assert!(result.is_err());
12669 let err_msg = format!("{}", result.unwrap_err());
12670 assert!(
12671 err_msg.contains("Circular") || err_msg.contains("circular"),
12672 "Expected circular import error, got: {err_msg}"
12673 );
12674 }
12675
12676 #[test]
12677 fn test_integration_use_aliased_method_call() {
12678 let dir = tempfile::tempdir().unwrap();
12680 std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12681
12682 let main_src = "use mylib as m\nprint(m.compute())";
12683 let main_path = dir.path().join("main.tl");
12684 std::fs::write(&main_path, main_src).unwrap();
12685
12686 let program = tl_parser::parse(main_src).unwrap();
12687 let proto = crate::compiler::compile(&program).unwrap();
12688 let mut vm = Vm::new();
12689 vm.file_path = Some(main_path.to_string_lossy().to_string());
12690 vm.execute(&proto).unwrap();
12691 assert_eq!(vm.output, vec!["42"]);
12692 }
12693
12694 #[test]
12695 fn test_integration_module_caching_shared() {
12696 let dir = tempfile::tempdir().unwrap();
12698 std::fs::write(dir.path().join("shared.tl"), "fn get_val() { 42 }").unwrap();
12699
12700 let main_src = "use shared\nprint(get_val())\nuse shared\nprint(get_val())";
12701 let main_path = dir.path().join("main.tl");
12702 std::fs::write(&main_path, main_src).unwrap();
12703
12704 let program = tl_parser::parse(main_src).unwrap();
12705 let proto = crate::compiler::compile(&program).unwrap();
12706 let mut vm = Vm::new();
12707 vm.file_path = Some(main_path.to_string_lossy().to_string());
12708 vm.execute(&proto).unwrap();
12709 assert_eq!(vm.output, vec!["42", "42"]);
12710 }
12711
12712 #[test]
12713 fn test_integration_pub_keyword_in_module() {
12714 let dir = tempfile::tempdir().unwrap();
12716 std::fs::write(
12717 dir.path().join("pubmod.tl"),
12718 "pub fn public_fn() { 100 }\nfn private_fn() { 200 }",
12719 )
12720 .unwrap();
12721
12722 let main_src = "use pubmod\nprint(public_fn())";
12723 let main_path = dir.path().join("main.tl");
12724 std::fs::write(&main_path, main_src).unwrap();
12725
12726 let program = tl_parser::parse(main_src).unwrap();
12727 let proto = crate::compiler::compile(&program).unwrap();
12728 let mut vm = Vm::new();
12729 vm.file_path = Some(main_path.to_string_lossy().to_string());
12730 vm.execute(&proto).unwrap();
12731 assert_eq!(vm.output, vec!["100"]);
12732 }
12733
12734 #[test]
12735 fn test_integration_backward_compat_import_as() {
12736 let dir = tempfile::tempdir().unwrap();
12738 let lib_path = dir.path().join("mylib.tl");
12739 std::fs::write(&lib_path, "fn compute() { 77 }").unwrap();
12740
12741 let main_src = format!(
12742 "import \"{}\" as m\nprint(m.compute())",
12743 lib_path.to_string_lossy()
12744 );
12745 let program = tl_parser::parse(&main_src).unwrap();
12746 let proto = crate::compiler::compile(&program).unwrap();
12747 let mut vm = Vm::new();
12748 vm.execute(&proto).unwrap();
12749 assert_eq!(vm.output, vec!["77"]);
12750 }
12751
12752 #[test]
12755 fn test_vm_generic_fn() {
12756 let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(42))");
12757 assert_eq!(output, vec!["42"]);
12758 }
12759
12760 #[test]
12761 fn test_vm_generic_fn_string() {
12762 let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(\"hello\"))");
12763 assert_eq!(output, vec!["hello"]);
12764 }
12765
12766 #[test]
12767 fn test_vm_generic_struct() {
12768 let output = run_output(
12769 "struct Pair<A, B> { first: A, second: B }\nlet p = Pair { first: 1, second: \"hi\" }\nprint(p.first)\nprint(p.second)",
12770 );
12771 assert_eq!(output, vec!["1", "hi"]);
12772 }
12773
12774 #[test]
12775 fn test_vm_trait_def_noop() {
12776 let output = run_output("trait Display { fn show(self) -> string }\nprint(\"ok\")");
12778 assert_eq!(output, vec!["ok"]);
12779 }
12780
12781 #[test]
12782 fn test_vm_trait_impl_methods() {
12783 let output = run_output(
12784 "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())",
12785 );
12786 assert_eq!(output, vec!["point"]);
12787 }
12788
12789 #[test]
12790 fn test_vm_generic_enum() {
12791 let output = run_output(
12793 "enum MyOpt<T> { Some(T), Nothing }\nlet x = MyOpt::Some(42)\nprint(type_of(x))",
12794 );
12795 assert_eq!(output, vec!["enum"]);
12796 }
12797
12798 #[test]
12799 fn test_vm_where_clause_runtime() {
12800 let output =
12802 run_output("fn compare<T>(x: T) where T: Comparable { x }\nprint(compare(10))");
12803 assert_eq!(output, vec!["10"]);
12804 }
12805
12806 #[test]
12807 fn test_vm_trait_impl_self_method() {
12808 let output = run_output(
12809 "struct Counter { value: int }\nimpl Incrementable for Counter { fn inc(self) { self.value + 1 } }\nlet c = Counter { value: 5 }\nprint(c.inc())",
12810 );
12811 assert_eq!(output, vec!["6"]);
12812 }
12813
12814 #[test]
12817 fn test_vm_generic_fn_with_type_inference() {
12818 let output = run_output(
12820 "fn first<T>(xs: list<T>) -> T { xs[0] }\nprint(first([1, 2, 3]))\nprint(first([\"a\", \"b\"]))",
12821 );
12822 assert_eq!(output, vec!["1", "a"]);
12823 }
12824
12825 #[test]
12826 fn test_vm_generic_struct_with_methods() {
12827 let output = run_output(
12828 "struct Box<T> { val: T }\nimpl Box { fn get(self) { self.val } }\nlet b = Box { val: 42 }\nprint(b.get())",
12829 );
12830 assert_eq!(output, vec!["42"]);
12831 }
12832
12833 #[test]
12834 fn test_vm_trait_def_impl_call() {
12835 let output = run_output(
12836 "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())",
12837 );
12838 assert_eq!(output, vec!["Alice"]);
12839 }
12840
12841 #[test]
12842 fn test_vm_multiple_generic_params() {
12843 let output = run_output(
12844 "fn pair<A, B>(a: A, b: B) { [a, b] }\nlet p = pair(1, \"two\")\nprint(len(p))",
12845 );
12846 assert_eq!(output, vec!["2"]);
12847 }
12848
12849 #[test]
12850 fn test_vm_backward_compat_non_generic() {
12851 let output = run_output(
12853 "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())",
12854 );
12855 assert_eq!(output, vec!["3", "7"]);
12856 }
12857
12858 #[test]
12861 fn test_vm_package_import_resolves() {
12862 let tmp = tempfile::tempdir().unwrap();
12864 let pkg_dir = tmp.path().join("mylib");
12865 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12866 std::fs::write(
12867 pkg_dir.join("src/lib.tl"),
12868 "pub fn greet() { print(\"hello from pkg\") }",
12869 )
12870 .unwrap();
12871 std::fs::write(
12872 pkg_dir.join("tl.toml"),
12873 "[project]\nname = \"mylib\"\nversion = \"1.0.0\"\n",
12874 )
12875 .unwrap();
12876
12877 let main_file = tmp.path().join("main.tl");
12879 std::fs::write(&main_file, "use mylib\ngreet()").unwrap();
12880
12881 let source = std::fs::read_to_string(&main_file).unwrap();
12882 let program = tl_parser::parse(&source).unwrap();
12883 let proto = crate::compiler::compile(&program).unwrap();
12884
12885 let mut vm = Vm::new();
12886 vm.file_path = Some(main_file.to_string_lossy().to_string());
12887 vm.package_roots.insert("mylib".into(), pkg_dir);
12888 vm.execute(&proto).unwrap();
12889
12890 assert_eq!(vm.output, vec!["hello from pkg"]);
12891 }
12892
12893 #[test]
12894 fn test_vm_package_nested_import() {
12895 let tmp = tempfile::tempdir().unwrap();
12896 let pkg_dir = tmp.path().join("utils");
12897 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12898 std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12899 std::fs::write(
12900 pkg_dir.join("tl.toml"),
12901 "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12902 )
12903 .unwrap();
12904
12905 let main_file = tmp.path().join("main.tl");
12907 std::fs::write(&main_file, "use utils.math\nprint(double(21))").unwrap();
12908
12909 let source = std::fs::read_to_string(&main_file).unwrap();
12910 let program = tl_parser::parse(&source).unwrap();
12911 let proto = crate::compiler::compile(&program).unwrap();
12912
12913 let mut vm = Vm::new();
12914 vm.file_path = Some(main_file.to_string_lossy().to_string());
12915 vm.package_roots.insert("utils".into(), pkg_dir);
12916 vm.execute(&proto).unwrap();
12917
12918 assert_eq!(vm.output, vec!["42"]);
12919 }
12920
12921 #[test]
12922 fn test_vm_package_aliased_import() {
12923 let tmp = tempfile::tempdir().unwrap();
12924 let pkg_dir = tmp.path().join("utils");
12925 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12926 std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12927 std::fs::write(
12928 pkg_dir.join("tl.toml"),
12929 "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12930 )
12931 .unwrap();
12932
12933 let main_file = tmp.path().join("main.tl");
12935 std::fs::write(&main_file, "use utils.math as m\nprint(m.double(21))").unwrap();
12936
12937 let source = std::fs::read_to_string(&main_file).unwrap();
12938 let program = tl_parser::parse(&source).unwrap();
12939 let proto = crate::compiler::compile(&program).unwrap();
12940
12941 let mut vm = Vm::new();
12942 vm.file_path = Some(main_file.to_string_lossy().to_string());
12943 vm.package_roots.insert("utils".into(), pkg_dir);
12944 vm.execute(&proto).unwrap();
12945
12946 assert_eq!(vm.output, vec!["42"]);
12947 }
12948
12949 #[test]
12950 fn test_vm_package_underscore_to_hyphen() {
12951 let tmp = tempfile::tempdir().unwrap();
12952 let pkg_dir = tmp.path().join("my-pkg");
12953 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12954 std::fs::write(pkg_dir.join("src/lib.tl"), "pub fn val() { print(99) }").unwrap();
12955 std::fs::write(
12956 pkg_dir.join("tl.toml"),
12957 "[project]\nname = \"my-pkg\"\nversion = \"1.0.0\"\n",
12958 )
12959 .unwrap();
12960
12961 let main_file = tmp.path().join("main.tl");
12963 std::fs::write(&main_file, "use my_pkg\nval()").unwrap();
12964
12965 let source = std::fs::read_to_string(&main_file).unwrap();
12966 let program = tl_parser::parse(&source).unwrap();
12967 let proto = crate::compiler::compile(&program).unwrap();
12968
12969 let mut vm = Vm::new();
12970 vm.file_path = Some(main_file.to_string_lossy().to_string());
12971 vm.package_roots.insert("my-pkg".into(), pkg_dir);
12972 vm.execute(&proto).unwrap();
12973
12974 assert_eq!(vm.output, vec!["99"]);
12975 }
12976
12977 #[test]
12978 fn test_vm_local_module_priority_over_package() {
12979 let tmp = tempfile::tempdir().unwrap();
12981
12982 std::fs::write(
12984 tmp.path().join("mymod.tl"),
12985 "pub fn val() { print(\"local\") }",
12986 )
12987 .unwrap();
12988
12989 let pkg_dir = tmp.path().join("pkg_mymod");
12991 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12992 std::fs::write(
12993 pkg_dir.join("src/lib.tl"),
12994 "pub fn val() { print(\"package\") }",
12995 )
12996 .unwrap();
12997
12998 let main_file = tmp.path().join("main.tl");
13000 std::fs::write(&main_file, "use mymod\nval()").unwrap();
13001
13002 let source = std::fs::read_to_string(&main_file).unwrap();
13003 let program = tl_parser::parse(&source).unwrap();
13004 let proto = crate::compiler::compile(&program).unwrap();
13005
13006 let mut vm = Vm::new();
13007 vm.file_path = Some(main_file.to_string_lossy().to_string());
13008 vm.package_roots.insert("mymod".into(), pkg_dir);
13009 vm.execute(&proto).unwrap();
13010
13011 assert_eq!(vm.output, vec!["local"]);
13013 }
13014
13015 #[test]
13016 fn test_vm_package_missing_error() {
13017 let tmp = tempfile::tempdir().unwrap();
13018 let main_file = tmp.path().join("main.tl");
13019 std::fs::write(&main_file, "use nonexistent\nnonexistent.foo()").unwrap();
13020
13021 let source = std::fs::read_to_string(&main_file).unwrap();
13022 let program = tl_parser::parse(&source).unwrap();
13023 let proto = crate::compiler::compile(&program).unwrap();
13024
13025 let mut vm = Vm::new();
13026 vm.file_path = Some(main_file.to_string_lossy().to_string());
13027 let result = vm.execute(&proto);
13028
13029 assert!(result.is_err());
13030 let err = format!("{:?}", result.unwrap_err());
13031 assert!(err.contains("Module not found"));
13032 }
13033
13034 #[test]
13035 #[cfg(feature = "native")]
13036 fn test_resolve_package_file_entry_points() {
13037 let tmp = tempfile::tempdir().unwrap();
13038
13039 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
13041 std::fs::write(tmp.path().join("src/lib.tl"), "").unwrap();
13042 let result = resolve_package_file(tmp.path(), &[]);
13043 assert!(result.is_some());
13044 assert!(result.unwrap().contains("lib.tl"));
13045
13046 std::fs::write(tmp.path().join("src/math.tl"), "").unwrap();
13048 let result = resolve_package_file(tmp.path(), &["math"]);
13049 assert!(result.is_some());
13050 assert!(result.unwrap().contains("math.tl"));
13051 }
13052
13053 #[test]
13054 fn test_vm_package_propagates_to_sub_imports() {
13055 let tmp = tempfile::tempdir().unwrap();
13057
13058 let pkg_dir = tmp.path().join("helpers");
13060 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13061 std::fs::write(
13062 pkg_dir.join("src/lib.tl"),
13063 "pub fn help() { print(\"helped\") }",
13064 )
13065 .unwrap();
13066 std::fs::write(
13067 pkg_dir.join("tl.toml"),
13068 "[project]\nname = \"helpers\"\nversion = \"1.0.0\"\n",
13069 )
13070 .unwrap();
13071
13072 std::fs::write(
13074 tmp.path().join("bridge.tl"),
13075 "use helpers\npub fn run() { help() }",
13076 )
13077 .unwrap();
13078
13079 let main_file = tmp.path().join("main.tl");
13081 std::fs::write(&main_file, "use bridge\nrun()").unwrap();
13082
13083 let source = std::fs::read_to_string(&main_file).unwrap();
13084 let program = tl_parser::parse(&source).unwrap();
13085 let proto = crate::compiler::compile(&program).unwrap();
13086
13087 let mut vm = Vm::new();
13088 vm.file_path = Some(main_file.to_string_lossy().to_string());
13089 vm.package_roots.insert("helpers".into(), pkg_dir);
13090 vm.execute(&proto).unwrap();
13091
13092 assert_eq!(vm.output, vec!["helped"]);
13093 }
13094
13095 #[test]
13098 fn test_block_body_closure_basic() {
13099 let output =
13100 run_output("let f = (x: int64) -> int64 { let y = x * 2\n y + 1 }\nprint(f(5))");
13101 assert_eq!(output, vec!["11"]);
13102 }
13103
13104 #[test]
13105 fn test_block_body_closure_captures_upvalue() {
13106 let output = run_output(
13107 "let offset = 10\nlet f = (x) -> int64 { let y = x + offset\n y }\nprint(f(5))",
13108 );
13109 assert_eq!(output, vec!["15"]);
13110 }
13111
13112 #[test]
13113 fn test_block_body_closure_as_hof_arg() {
13114 let output = run_output(
13115 "let nums = [1, 2, 3]\nlet result = map(nums, (x) -> int64 { let doubled = x * 2\n doubled + 1 })\nprint(result)",
13116 );
13117 assert_eq!(output, vec!["[3, 5, 7]"]);
13118 }
13119
13120 #[test]
13121 fn test_block_body_closure_multi_stmt() {
13122 let output = run_output(
13123 "let f = (a, b) -> int64 { let sum = a + b\n let product = a * b\n sum + product }\nprint(f(3, 4))",
13124 );
13125 assert_eq!(output, vec!["19"]);
13126 }
13127
13128 #[test]
13129 fn test_type_alias_noop() {
13130 let output = run_output(
13132 "type Mapper = fn(int64) -> int64\nlet f: Mapper = (x) => x * 2\nprint(f(5))",
13133 );
13134 assert_eq!(output, vec!["10"]);
13135 }
13136
13137 #[test]
13138 fn test_type_alias_in_function_sig() {
13139 let output = run_output(
13140 "type Mapper = fn(int64) -> int64\nfn apply(f: Mapper, x: int64) -> int64 { f(x) }\nprint(apply((x) => x + 10, 5))",
13141 );
13142 assert_eq!(output, vec!["15"]);
13143 }
13144
13145 #[test]
13146 fn test_shorthand_closure() {
13147 let output = run_output("let double = x => x * 2\nprint(double(5))");
13148 assert_eq!(output, vec!["10"]);
13149 }
13150
13151 #[test]
13152 fn test_shorthand_closure_in_map() {
13153 let output = run_output("let nums = [1, 2, 3]\nprint(map(nums, x => x * 2))");
13154 assert_eq!(output, vec!["[2, 4, 6]"]);
13155 }
13156
13157 #[test]
13158 fn test_iife() {
13159 let output = run_output("let r = ((x) => x * 2)(5)\nprint(r)");
13160 assert_eq!(output, vec!["10"]);
13161 }
13162
13163 #[test]
13164 fn test_hof_apply() {
13165 let output = run_output("fn apply(f, x) { f(x) }\nprint(apply((x) => x + 10, 5))");
13166 assert_eq!(output, vec!["15"]);
13167 }
13168
13169 #[test]
13170 fn test_closure_stored_in_list() {
13171 let output = run_output(
13172 "let fns = [(x) => x + 1, (x) => x * 2]\nprint(fns[0](5))\nprint(fns[1](5))",
13173 );
13174 assert_eq!(output, vec!["6", "10"]);
13175 }
13176
13177 #[test]
13178 fn test_block_body_closure_with_return() {
13179 let output = run_output(
13181 "let classify = (x) -> string { if x > 0 { return \"positive\" }\n \"non-positive\" }\nprint(classify(5))\nprint(classify(-1))",
13182 );
13183 assert_eq!(output, vec!["positive", "non-positive"]);
13184 }
13185
13186 #[test]
13187 fn test_shorthand_closure_in_filter() {
13188 let output = run_output(
13189 "let nums = [1, 2, 3, 4, 5, 6]\nlet evens = filter(nums, x => x % 2 == 0)\nprint(evens)",
13190 );
13191 assert_eq!(output, vec!["[2, 4, 6]"]);
13192 }
13193
13194 #[test]
13195 fn test_block_closure_with_multiple_returns() {
13196 let output = run_output(
13197 "let abs_val = (x) -> int64 { if x < 0 { return -x }\n x }\nprint(abs_val(-5))\nprint(abs_val(3))",
13198 );
13199 assert_eq!(output, vec!["5", "3"]);
13200 }
13201
13202 #[test]
13203 fn test_type_alias_with_block_closure() {
13204 let output = run_output(
13205 "type Transform = fn(int64) -> int64\nlet f: Transform = (x) -> int64 { let y = x * x\n y + 1 }\nprint(f(3))",
13206 );
13207 assert_eq!(output, vec!["10"]);
13208 }
13209
13210 #[test]
13211 fn test_closure_both_backends_expr() {
13212 let output = run_output("let f = (x) => x * 3 + 1\nprint(f(4))");
13214 assert_eq!(output, vec!["13"]);
13215 }
13216
13217 #[test]
13219 #[cfg(not(feature = "python"))]
13220 fn test_py_feature_disabled() {
13221 let result = run("py_import(\"math\")");
13222 assert!(result.is_err());
13223 let msg = format!("{}", result.unwrap_err());
13224 assert!(msg.contains("python") && msg.contains("feature"));
13225 }
13226
13227 #[test]
13228 #[cfg(feature = "python")]
13229 fn test_vm_py_import_and_eval() {
13230 pyo3::prepare_freethreaded_python();
13231 let output = run_output("let m = py_import(\"math\")\nlet pi = m.pi\nprint(pi)");
13232 assert_eq!(output.len(), 1);
13233 let pi: f64 = output[0].parse().unwrap();
13234 assert!((pi - std::f64::consts::PI).abs() < 1e-10);
13235 }
13236
13237 #[test]
13238 #[cfg(feature = "python")]
13239 fn test_vm_py_eval_arithmetic() {
13240 pyo3::prepare_freethreaded_python();
13241 let output = run_output("let x = py_eval(\"2 ** 10\")\nprint(x)");
13242 assert_eq!(output, vec!["1024"]);
13243 }
13244
13245 #[test]
13246 #[cfg(feature = "python")]
13247 fn test_vm_py_method_dispatch() {
13248 pyo3::prepare_freethreaded_python();
13249 let output = run_output("let m = py_import(\"math\")\nprint(m.sqrt(25.0))");
13250 assert_eq!(output, vec!["5.0"]);
13251 }
13252
13253 #[test]
13254 #[cfg(feature = "python")]
13255 fn test_vm_py_list_conversion() {
13256 pyo3::prepare_freethreaded_python();
13257 let output = run_output("let x = py_eval(\"[10, 20, 30]\")\nprint(x)");
13258 assert_eq!(output, vec!["[10, 20, 30]"]);
13259 }
13260
13261 #[test]
13262 #[cfg(feature = "python")]
13263 fn test_vm_py_none_conversion() {
13264 pyo3::prepare_freethreaded_python();
13265 let output = run_output("let x = py_eval(\"None\")\nprint(x)");
13266 assert_eq!(output, vec!["none"]);
13267 }
13268
13269 #[test]
13270 #[cfg(feature = "python")]
13271 fn test_vm_py_error_msg_quality() {
13272 pyo3::prepare_freethreaded_python();
13273 let result = run("py_import(\"nonexistent_xyz_module\")");
13274 assert!(result.is_err());
13275 let msg = format!("{}", result.unwrap_err());
13276 assert!(msg.contains("py_import") && msg.contains("nonexistent_xyz_module"));
13277 }
13278
13279 #[test]
13280 #[cfg(feature = "python")]
13281 fn test_vm_py_getattr_setattr() {
13282 pyo3::prepare_freethreaded_python();
13283 let output = run_output(
13284 "let t = py_import(\"types\")\nlet obj = py_call(py_getattr(t, \"SimpleNamespace\"))\npy_setattr(obj, \"val\", 99)\nprint(py_getattr(obj, \"val\"))",
13285 );
13286 assert_eq!(output, vec!["99"]);
13287 }
13288
13289 #[test]
13290 #[cfg(feature = "python")]
13291 fn test_vm_py_callable_round_trip() {
13292 pyo3::prepare_freethreaded_python();
13293 let output = run_output(
13294 "let m = py_import(\"math\")\nlet f = py_getattr(m, \"floor\")\nprint(py_call(f, 3.7))",
13295 );
13296 assert_eq!(output, vec!["3"]);
13297 }
13298
13299 #[test]
13302 fn test_vm_schema_register_and_get() {
13303 let source = r#"let fields = map_from("id", "int64", "name", "string")
13304schema_register("User", 1, fields)
13305let result = schema_get("User", 1)
13306print(len(result))"#;
13307 let output = run_output(source);
13308 assert_eq!(output, vec!["2"]);
13309 }
13310
13311 #[test]
13312 fn test_vm_schema_latest() {
13313 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13314schema_register("User", 2, map_from("id", "int64", "name", "string"))
13315let latest = schema_latest("User")
13316print(latest)"#;
13317 let output = run_output(source);
13318 assert_eq!(output, vec!["2"]);
13319 }
13320
13321 #[test]
13322 fn test_vm_schema_history() {
13323 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13324schema_register("User", 2, map_from("id", "int64", "name", "string"))
13325let hist = schema_history("User")
13326print(len(hist))"#;
13327 let output = run_output(source);
13328 assert_eq!(output, vec!["2"]);
13329 }
13330
13331 #[test]
13332 fn test_vm_schema_check_backward_compat() {
13333 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13334schema_register("User", 2, map_from("id", "int64", "name", "string"))
13335let issues = schema_check("User", 1, 2, "backward")
13336print(len(issues))"#;
13337 let output = run_output(source);
13338 assert_eq!(output, vec!["0"]);
13339 }
13340
13341 #[test]
13342 fn test_vm_schema_diff() {
13343 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13344schema_register("User", 2, map_from("id", "int64", "name", "string"))
13345let diffs = schema_diff("User", 1, 2)
13346print(len(diffs))"#;
13347 let output = run_output(source);
13348 assert_eq!(output, vec!["1"]);
13349 }
13350
13351 #[test]
13352 fn test_vm_schema_versions() {
13353 let source = r#"schema_register("T", 1, map_from("id", "int64"))
13354schema_register("T", 3, map_from("id", "int64"))
13355schema_register("T", 2, map_from("id", "int64"))
13356let vers = schema_versions("T")
13357print(len(vers))"#;
13358 let output = run_output(source);
13359 assert_eq!(output, vec!["3"]);
13360 }
13361
13362 #[test]
13363 fn test_vm_schema_fields() {
13364 let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13365let fields = schema_fields("User", 1)
13366print(len(fields))"#;
13367 let output = run_output(source);
13368 assert_eq!(output, vec!["2"]);
13369 }
13370
13371 #[test]
13372 fn test_vm_compile_versioned_schema() {
13373 let source = "/// @version 1\nschema User { id: int64, name: string }\nprint(User)";
13374 let output = run_output(source);
13375 assert!(output[0].contains("__schema__:User:v1:"));
13376 }
13377
13378 #[test]
13379 fn test_vm_compile_migrate() {
13380 let source = "migrate User from 1 to 2 { add_column(email: string) }\nprint(\"ok\")";
13381 let output = run_output(source);
13382 assert_eq!(output, vec!["ok"]);
13383 }
13384
13385 #[test]
13386 fn test_vm_schema_check_backward_compat_fails() {
13387 let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13388schema_register("User", 2, map_from("id", "int64"))
13389let issues = schema_check("User", 1, 2, "backward")
13390print(len(issues))"#;
13391 let output = run_output(source);
13392 assert_eq!(output, vec!["1"]);
13393 }
13394
13395 #[test]
13398 fn test_vm_decimal_literal_and_arithmetic() {
13399 let output = run_output("let a = 10.5d\nlet b = 2.5d\nprint(a + b)\nprint(a * b)");
13400 assert_eq!(output, vec!["13.0", "26.25"]);
13401 }
13402
13403 #[test]
13404 fn test_vm_decimal_div_by_zero() {
13405 let source = "let a = 1.0d\nlet b = 0.0d\nlet c = a / b";
13406 let program = tl_parser::parse(source).unwrap();
13407 let proto = crate::compile(&program).unwrap();
13408 let mut vm = Vm::new();
13409 let result = vm.execute(&proto);
13410 assert!(result.is_err());
13411 }
13412
13413 #[test]
13414 fn test_vm_decimal_comparison_ops() {
13415 let output =
13416 run_output("let a = 1.0d\nlet b = 2.0d\nprint(a < b)\nprint(a >= b)\nprint(a == a)");
13417 assert_eq!(output, vec!["true", "false", "true"]);
13418 }
13419
13420 #[test]
13423 fn test_vm_secret_vault_crud() {
13424 let output = run_output(
13425 "secret_set(\"key\", \"value\")\nlet s = secret_get(\"key\")\nprint(s)\nsecret_delete(\"key\")\nlet s2 = secret_get(\"key\")\nprint(type_of(s2))",
13426 );
13427 assert_eq!(output, vec!["***", "none"]);
13428 }
13429
13430 #[test]
13431 fn test_vm_mask_email_basic() {
13432 let output = run_output("print(mask_email(\"alice@domain.com\"))");
13433 assert_eq!(output, vec!["a***@domain.com"]);
13434 }
13435
13436 #[test]
13437 fn test_vm_mask_phone_basic() {
13438 let output = run_output("print(mask_phone(\"123-456-7890\"))");
13439 assert_eq!(output, vec!["***-***-7890"]);
13440 }
13441
13442 #[test]
13443 fn test_vm_mask_cc_basic() {
13444 let output = run_output("print(mask_cc(\"4111222233334444\"))");
13445 assert_eq!(output, vec!["****-****-****-4444"]);
13446 }
13447
13448 #[test]
13449 fn test_vm_hash_produces_hex() {
13450 let output = run_output("let h = hash(\"test\", \"sha256\")\nprint(len(h))");
13451 assert_eq!(output, vec!["64"]);
13452 }
13453
13454 #[test]
13455 fn test_vm_redact_modes() {
13456 let output =
13457 run_output("print(redact(\"hello\", \"full\"))\nprint(redact(\"hello\", \"partial\"))");
13458 assert_eq!(output, vec!["***", "h***o"]);
13459 }
13460
13461 #[test]
13462 fn test_vm_security_policy_sandbox() {
13463 let source = "print(check_permission(\"network\"))\nprint(check_permission(\"file_read\"))";
13464 let program = tl_parser::parse(source).unwrap();
13465 let proto = crate::compile(&program).unwrap();
13466 let mut vm = Vm::new();
13467 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13468 vm.execute(&proto).unwrap();
13469 assert_eq!(vm.output, vec!["false", "true"]);
13470 }
13471
13472 #[cfg(feature = "async-runtime")]
13475 #[test]
13476 fn test_vm_async_read_write_file() {
13477 let dir = tempfile::tempdir().unwrap();
13478 let path = dir.path().join("async_test.txt");
13479 let path_str = path.to_str().unwrap().replace('\\', "/");
13480 let source = format!(
13481 r#"let wt = async_write_file("{path_str}", "async hello")
13482let wr = await(wt)
13483let rt = async_read_file("{path_str}")
13484let content = await(rt)
13485print(content)"#
13486 );
13487 let output = run_output(&source);
13488 assert_eq!(output, vec!["async hello"]);
13489 }
13490
13491 #[cfg(feature = "async-runtime")]
13492 #[test]
13493 fn test_vm_async_sleep() {
13494 let source = r#"
13495let t = async_sleep(10)
13496let r = await(t)
13497print(r)
13498"#;
13499 let output = run_output(source);
13500 assert_eq!(output, vec!["none"]);
13501 }
13502
13503 #[cfg(feature = "async-runtime")]
13504 #[test]
13505 fn test_vm_select_first_wins() {
13506 let source = r#"
13508let fast = async_sleep(10)
13509let slow = async_sleep(5000)
13510let winner = select(fast, slow)
13511let result = await(winner)
13512print(result)
13513"#;
13514 let output = run_output(source);
13515 assert_eq!(output, vec!["none"]);
13516 }
13517
13518 #[cfg(feature = "async-runtime")]
13519 #[test]
13520 fn test_vm_race_all() {
13521 let source = r#"
13522let t1 = async_sleep(10)
13523let t2 = async_sleep(5000)
13524let winner = race_all([t1, t2])
13525let result = await(winner)
13526print(result)
13527"#;
13528 let output = run_output(source);
13529 assert_eq!(output, vec!["none"]);
13530 }
13531
13532 #[cfg(feature = "async-runtime")]
13533 #[test]
13534 fn test_vm_async_map() {
13535 let source = r#"
13536let items = [1, 2, 3]
13537let t = async_map(items, (x) => x * 10)
13538let result = await(t)
13539print(result)
13540"#;
13541 let output = run_output(source);
13542 assert_eq!(output, vec!["[10, 20, 30]"]);
13543 }
13544
13545 #[cfg(feature = "async-runtime")]
13546 #[test]
13547 fn test_vm_async_filter() {
13548 let source = r#"
13549let items = [1, 2, 3, 4, 5]
13550let t = async_filter(items, (x) => x > 3)
13551let result = await(t)
13552print(result)
13553"#;
13554 let output = run_output(source);
13555 assert_eq!(output, vec!["[4, 5]"]);
13556 }
13557
13558 #[cfg(feature = "async-runtime")]
13559 #[test]
13560 fn test_vm_async_write_file_returns_none() {
13561 let dir = tempfile::tempdir().unwrap();
13562 let path = dir.path().join("write_test.txt");
13563 let path_str = path.to_str().unwrap().replace('\\', "/");
13564 let source = format!(
13565 r#"let t = async_write_file("{path_str}", "test data")
13566let r = await(t)
13567print(r)"#
13568 );
13569 let output = run_output(&source);
13570 assert_eq!(output, vec!["none"]);
13571 }
13572
13573 #[cfg(feature = "async-runtime")]
13574 #[test]
13575 fn test_vm_async_security_policy_blocks_write() {
13576 let source = r#"let t = async_write_file("/tmp/blocked.txt", "data")"#;
13577 let program = tl_parser::parse(source).unwrap();
13578 let proto = crate::compile(&program).unwrap();
13579 let mut vm = Vm::new();
13580 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13581 let result = vm.execute(&proto);
13582 assert!(result.is_err());
13583 let err = format!("{}", result.unwrap_err());
13584 assert!(
13585 err.contains("file_write not allowed"),
13586 "Expected security error, got: {err}"
13587 );
13588 }
13589
13590 #[cfg(feature = "async-runtime")]
13591 #[test]
13592 fn test_vm_async_security_policy_allows_read() {
13593 let dir = tempfile::tempdir().unwrap();
13595 let path = dir.path().join("readable.txt");
13596 std::fs::write(&path, "safe content").unwrap();
13597 let path_str = path.to_str().unwrap().replace('\\', "/");
13598 let source = format!(
13599 r#"let t = async_read_file("{path_str}")
13600let r = await(t)
13601print(r)"#
13602 );
13603 let program = tl_parser::parse(&source).unwrap();
13604 let proto = crate::compile(&program).unwrap();
13605 let mut vm = Vm::new();
13606 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13607 vm.execute(&proto).unwrap();
13608 assert_eq!(vm.output, vec!["safe content"]);
13609 }
13610
13611 #[cfg(feature = "async-runtime")]
13612 #[test]
13613 fn test_vm_async_map_empty_list() {
13614 let source = r#"
13615let t = async_map([], (x) => x * 2)
13616let result = await(t)
13617print(result)
13618"#;
13619 let output = run_output(source);
13620 assert_eq!(output, vec!["[]"]);
13621 }
13622
13623 #[cfg(feature = "async-runtime")]
13624 #[test]
13625 fn test_vm_async_filter_none_match() {
13626 let source = r#"
13627let t = async_filter([1, 2, 3], (x) => x > 100)
13628let result = await(t)
13629print(result)
13630"#;
13631 let output = run_output(source);
13632 assert_eq!(output, vec!["[]"]);
13633 }
13634
13635 #[test]
13638 fn test_vm_closure_returned_from_function() {
13639 let output = run_output(
13640 r#"
13641fn make_adder(n) {
13642 return (x) => x + n
13643}
13644let add5 = make_adder(5)
13645print(add5(3))
13646print(add5(10))
13647"#,
13648 );
13649 assert_eq!(output, vec!["8", "15"]);
13650 }
13651
13652 #[test]
13653 fn test_vm_closure_factory_multiple_calls() {
13654 let output = run_output(
13655 r#"
13656fn make_adder(n) {
13657 return (x) => x + n
13658}
13659let add2 = make_adder(2)
13660let add10 = make_adder(10)
13661print(add2(5))
13662print(add10(5))
13663print(add2(1))
13664"#,
13665 );
13666 assert_eq!(output, vec!["7", "15", "3"]);
13667 }
13668
13669 #[test]
13670 fn test_vm_closure_returned_in_list() {
13671 let output = run_output(
13672 r#"
13673fn make_ops(n) {
13674 let add = (x) => x + n
13675 let mul = (x) => x * n
13676 return [add, mul]
13677}
13678let ops = make_ops(3)
13679print(ops[0](10))
13680print(ops[1](10))
13681"#,
13682 );
13683 assert_eq!(output, vec!["13", "30"]);
13684 }
13685
13686 #[test]
13687 fn test_vm_nested_closure_return() {
13688 let output = run_output(
13689 r#"
13690fn outer(a) {
13691 fn inner(b) {
13692 return (x) => x + a + b
13693 }
13694 return inner(10)
13695}
13696let f = outer(5)
13697print(f(1))
13698"#,
13699 );
13700 assert_eq!(output, vec!["16"]);
13701 }
13702
13703 #[test]
13704 fn test_vm_multiple_closures_same_local() {
13705 let output = run_output(
13706 r#"
13707fn make_pair(n) {
13708 let inc = (x) => x + n
13709 let dec = (x) => x - n
13710 return [inc, dec]
13711}
13712let pair = make_pair(7)
13713print(pair[0](10))
13714print(pair[1](10))
13715"#,
13716 );
13717 assert_eq!(output, vec!["17", "3"]);
13718 }
13719
13720 #[test]
13721 fn test_vm_closure_captures_multiple_locals() {
13722 let output = run_output(
13723 r#"
13724fn make_greeter(greeting, name) {
13725 let sep = " "
13726 return () => greeting + sep + name
13727}
13728let hi = make_greeter("Hello", "World")
13729let bye = make_greeter("Goodbye", "Alice")
13730print(hi())
13731print(bye())
13732"#,
13733 );
13734 assert_eq!(output, vec!["Hello World", "Goodbye Alice"]);
13735 }
13736
13737 #[test]
13740 fn test_vm_throw_catch_preserves_enum() {
13741 let output = run_output(
13742 r#"
13743enum Color { Red, Green(x) }
13744try {
13745 throw Color::Green(42)
13746} catch e {
13747 match e {
13748 Color::Green(x) => print(x),
13749 _ => print("no match"),
13750 }
13751}
13752"#,
13753 );
13754 assert_eq!(output, vec!["42"]);
13755 }
13756
13757 #[test]
13758 fn test_vm_throw_catch_string_compat() {
13759 let output = run_output(
13760 r#"
13761try {
13762 throw "hello error"
13763} catch e {
13764 print(e)
13765}
13766"#,
13767 );
13768 assert_eq!(output, vec!["hello error"]);
13769 }
13770
13771 #[test]
13772 fn test_vm_runtime_error_still_string() {
13773 let output = run_output(
13774 r#"
13775try {
13776 let x = 1 / 0
13777} catch e {
13778 print(type_of(e))
13779}
13780"#,
13781 );
13782 assert_eq!(output, vec!["string"]);
13783 }
13784
13785 #[test]
13786 fn test_vm_data_error_construct_and_throw() {
13787 let output = run_output(
13788 r#"
13789try {
13790 throw DataError::ParseError("bad format", "file.csv")
13791} catch e {
13792 print(match e { DataError::ParseError(msg, _) => msg, _ => "no match" })
13793 print(match e { DataError::ParseError(_, src) => src, _ => "no match" })
13794}
13795"#,
13796 );
13797 assert_eq!(output, vec!["bad format", "file.csv"]);
13798 }
13799
13800 #[test]
13801 fn test_vm_network_error_construct() {
13802 let output = run_output(
13803 r#"
13804let err = NetworkError::TimeoutError("timed out")
13805match err {
13806 NetworkError::TimeoutError(msg) => print(msg),
13807 _ => print("no match"),
13808}
13809"#,
13810 );
13811 assert_eq!(output, vec!["timed out"]);
13812 }
13813
13814 #[test]
13815 fn test_vm_connector_error_construct() {
13816 let output = run_output(
13817 r#"
13818let err = ConnectorError::AuthError("invalid creds", "postgres")
13819print(match err { ConnectorError::AuthError(msg, _) => msg, _ => "no match" })
13820print(match err { ConnectorError::AuthError(_, conn) => conn, _ => "no match" })
13821"#,
13822 );
13823 assert_eq!(output, vec!["invalid creds", "postgres"]);
13824 }
13825
13826 #[test]
13827 fn test_vm_is_error_builtin() {
13828 let output = run_output(
13829 r#"
13830let e1 = DataError::NotFound("users")
13831let e2 = NetworkError::TimeoutError("slow")
13832let e3 = ConnectorError::ConfigError("bad", "redis")
13833let e4 = "not an error"
13834print(is_error(e1))
13835print(is_error(e2))
13836print(is_error(e3))
13837print(is_error(e4))
13838"#,
13839 );
13840 assert_eq!(output, vec!["true", "true", "true", "false"]);
13841 }
13842
13843 #[test]
13844 fn test_vm_error_type_builtin() {
13845 let output = run_output(
13846 r#"
13847let e1 = DataError::ParseError("bad", "x.csv")
13848let e2 = NetworkError::HttpError("fail", "url")
13849let e3 = "not an error"
13850print(error_type(e1))
13851print(error_type(e2))
13852print(error_type(e3))
13853"#,
13854 );
13855 assert_eq!(output, vec!["DataError", "NetworkError", "none"]);
13856 }
13857
13858 #[test]
13859 fn test_vm_match_error_variants() {
13860 let output = run_output(
13861 r#"
13862fn handle(err) {
13863 print(match err {
13864 DataError::ParseError(msg, _) => "parse: " + msg,
13865 DataError::SchemaError(msg, _, _) => "schema: " + msg,
13866 DataError::ValidationError(_, field) => "validation: " + field,
13867 DataError::NotFound(name) => "not found: " + name,
13868 _ => "unknown"
13869 })
13870}
13871handle(DataError::ParseError("bad csv", "data.csv"))
13872handle(DataError::NotFound("users_table"))
13873handle(DataError::SchemaError("mismatch", "int", "string"))
13874handle(DataError::ValidationError("invalid", "email"))
13875"#,
13876 );
13877 assert_eq!(
13878 output,
13879 vec![
13880 "parse: bad csv",
13881 "not found: users_table",
13882 "schema: mismatch",
13883 "validation: email",
13884 ]
13885 );
13886 }
13887
13888 #[test]
13889 fn test_vm_rethrow_structured_error() {
13890 let output = run_output(
13891 r#"
13892try {
13893 try {
13894 throw DataError::NotFound("config")
13895 } catch e {
13896 throw e
13897 }
13898} catch outer {
13899 match outer {
13900 DataError::NotFound(name) => print("caught: " + name),
13901 _ => print("wrong type"),
13902 }
13903}
13904"#,
13905 );
13906 assert_eq!(output, vec!["caught: config"]);
13907 }
13908
13909 #[test]
13912 fn test_vm_pipe_moves_value() {
13913 let result = run(r#"
13915fn identity(v) { v }
13916let x = [1, 2, 3]
13917x |> identity()
13918print(x)
13919"#);
13920 assert!(result.is_err());
13921 let err = result.unwrap_err().to_string();
13922 assert!(err.contains("moved"), "Error should mention 'moved': {err}");
13923 }
13924
13925 #[test]
13926 fn test_vm_clone_before_pipe() {
13927 let output = run_output(
13929 r#"
13930fn identity(v) { v }
13931let x = [1, 2, 3]
13932x.clone() |> identity()
13933print(x)
13934"#,
13935 );
13936 assert_eq!(output, vec!["[1, 2, 3]"]);
13937 }
13938
13939 #[test]
13940 fn test_vm_clone_list_deep() {
13941 let output = run_output(
13943 r#"
13944let original = [1, 2, 3]
13945let copy = original.clone()
13946copy[0] = 99
13947print(original)
13948print(copy)
13949"#,
13950 );
13951 assert_eq!(output, vec!["[1, 2, 3]", "[99, 2, 3]"]);
13952 }
13953
13954 #[test]
13955 fn test_vm_clone_map() {
13956 let output = run_output(
13957 r#"
13958let m = map_from("a", 1, "b", 2)
13959let m2 = m.clone()
13960m2["a"] = 99
13961print(m)
13962print(m2)
13963"#,
13964 );
13965 assert_eq!(output, vec!["{a: 1, b: 2}", "{a: 99, b: 2}"]);
13966 }
13967
13968 #[test]
13969 fn test_vm_clone_struct() {
13970 let output = run_output(
13971 r#"
13972struct Point { x: int64, y: int64 }
13973let p = Point { x: 1, y: 2 }
13974let p2 = p.clone()
13975print(p)
13976print(p2)
13977"#,
13978 );
13979 assert_eq!(output, vec!["Point { x: 1, y: 2 }", "Point { x: 1, y: 2 }"]);
13980 }
13981
13982 #[test]
13983 fn test_vm_ref_read_only() {
13984 let result = run(r#"
13986let x = [1, 2, 3]
13987let r = &x
13988r[0] = 99
13989"#);
13990 assert!(result.is_err());
13991 let err = result.unwrap_err().to_string();
13992 assert!(
13993 err.contains("Cannot mutate a borrowed reference"),
13994 "Error should mention reference: {err}"
13995 );
13996 }
13997
13998 #[test]
13999 fn test_vm_ref_transparent_read() {
14000 let output = run_output(
14002 r#"
14003let x = [1, 2, 3]
14004let r = &x
14005print(len(r))
14006"#,
14007 );
14008 assert_eq!(output, vec!["3"]);
14009 }
14010
14011 #[test]
14012 fn test_vm_parallel_for_basic() {
14013 let output = run_output(
14015 r#"
14016let items = [10, 20, 30]
14017parallel for item in items {
14018 print(item)
14019}
14020"#,
14021 );
14022 assert_eq!(output, vec!["10", "20", "30"]);
14023 }
14024
14025 #[test]
14026 fn test_vm_moved_value_clear_error() {
14027 let result = run(r#"
14029fn f(x) { x }
14030let data = "hello"
14031data |> f()
14032print(data)
14033"#);
14034 assert!(result.is_err());
14035 let err = result.unwrap_err().to_string();
14036 assert!(
14037 err.contains("clone()"),
14038 "Error should suggest .clone(): {err}"
14039 );
14040 }
14041
14042 #[test]
14043 fn test_vm_reassign_after_move() {
14044 let output = run_output(
14046 r#"
14047fn f(x) { x }
14048let x = 1
14049x |> f()
14050let x = 2
14051print(x)
14052"#,
14053 );
14054 assert_eq!(output, vec!["2"]);
14055 }
14056
14057 #[test]
14058 fn test_vm_pipe_chain_move() {
14059 let output = run_output(
14061 r#"
14062fn double(x) { x * 2 }
14063fn add_one(x) { x + 1 }
14064let result = 5 |> double() |> add_one()
14065print(result)
14066"#,
14067 );
14068 assert_eq!(output, vec!["11"]);
14069 }
14070
14071 #[test]
14072 fn test_vm_string_clone() {
14073 let output = run_output(
14075 r#"
14076let s = "hello"
14077let s2 = s.clone()
14078print(s)
14079print(s2)
14080"#,
14081 );
14082 assert_eq!(output, vec!["hello", "hello"]);
14083 }
14084
14085 #[test]
14086 fn test_vm_ref_method_dispatch() {
14087 let output = run_output(
14089 r#"
14090let s = "hello world"
14091let r = &s
14092print(r.len())
14093"#,
14094 );
14095 assert_eq!(output, vec!["11"]);
14096 }
14097
14098 #[test]
14099 fn test_vm_ref_member_access() {
14100 let output = run_output(
14102 r#"
14103struct Point { x: int64, y: int64 }
14104let p = Point { x: 10, y: 20 }
14105let r = &p
14106print(r.x)
14107"#,
14108 );
14109 assert_eq!(output, vec!["10"]);
14110 }
14111
14112 #[test]
14113 fn test_vm_ref_set_member_blocked() {
14114 let result = run(r#"
14116struct Point { x: int64, y: int64 }
14117let p = Point { x: 10, y: 20 }
14118let r = &p
14119r.x = 99
14120"#);
14121 assert!(result.is_err());
14122 let err = result.unwrap_err().to_string();
14123 assert!(
14124 err.contains("Cannot mutate a borrowed reference"),
14125 "Error: {err}"
14126 );
14127 }
14128
14129 #[test]
14132 fn test_ir_filter_merge_chain() {
14133 let dir = tempfile::tempdir().unwrap();
14135 let csv = dir.path().join("data.csv");
14136 std::fs::write(&csv, "name,age\nAlice,30\nBob,20\nCharlie,35\n").unwrap();
14137 let src = format!(
14138 r#"let t = read_csv("{}")
14139let r = t |> filter(age > 25) |> filter(age < 40) |> collect()
14140print(r)"#,
14141 csv.to_str().unwrap()
14142 );
14143 let output = run_output(&src);
14144 assert!(
14146 output[0].contains("Alice"),
14147 "Output should contain Alice: {}",
14148 output[0]
14149 );
14150 assert!(
14151 output[0].contains("Charlie"),
14152 "Output should contain Charlie: {}",
14153 output[0]
14154 );
14155 assert!(
14156 !output[0].contains("Bob"),
14157 "Output should not contain Bob: {}",
14158 output[0]
14159 );
14160 }
14161
14162 #[test]
14163 fn test_ir_predicate_pushdown_through_select() {
14164 let dir = tempfile::tempdir().unwrap();
14166 let csv = dir.path().join("data.csv");
14167 std::fs::write(
14168 &csv,
14169 "name,age,city\nAlice,30,NYC\nBob,20,LA\nCharlie,35,NYC\n",
14170 )
14171 .unwrap();
14172 let src = format!(
14173 r#"let t = read_csv("{}")
14174let r = t |> select(name, age) |> filter(age > 25) |> collect()
14175print(r)"#,
14176 csv.to_str().unwrap()
14177 );
14178 let output = run_output(&src);
14179 assert!(output[0].contains("Alice"), "Output should contain Alice");
14180 assert!(
14181 output[0].contains("Charlie"),
14182 "Output should contain Charlie"
14183 );
14184 assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14185 }
14186
14187 #[test]
14188 fn test_ir_sort_filter_pushdown() {
14189 let dir = tempfile::tempdir().unwrap();
14191 let csv = dir.path().join("data.csv");
14192 std::fs::write(&csv, "name,score\nAlice,90\nBob,50\nCharlie,75\n").unwrap();
14193 let src = format!(
14194 r#"let t = read_csv("{}")
14195let r = t |> sort(score, "desc") |> filter(score > 60) |> collect()
14196print(r)"#,
14197 csv.to_str().unwrap()
14198 );
14199 let output = run_output(&src);
14200 assert!(output[0].contains("Alice"), "Output should contain Alice");
14201 assert!(
14202 output[0].contains("Charlie"),
14203 "Output should contain Charlie"
14204 );
14205 assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14206 }
14207
14208 #[test]
14209 fn test_ir_multi_operation_chain() {
14210 let dir = tempfile::tempdir().unwrap();
14212 let csv = dir.path().join("data.csv");
14213 std::fs::write(
14214 &csv,
14215 "name,age,dept\nAlice,30,Eng\nBob,20,Sales\nCharlie,35,Eng\nDiana,28,Sales\n",
14216 )
14217 .unwrap();
14218 let src = format!(
14219 r#"let t = read_csv("{}")
14220let r = t |> filter(age > 22) |> select(name, age) |> sort(age, "desc") |> limit(2) |> collect()
14221print(r)"#,
14222 csv.to_str().unwrap()
14223 );
14224 let output = run_output(&src);
14225 assert!(output[0].contains("Charlie"), "Output: {}", output[0]);
14227 assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14228 }
14229
14230 #[test]
14231 fn test_ir_pipe_move_semantics_preserved() {
14232 let dir = tempfile::tempdir().unwrap();
14234 let csv = dir.path().join("data.csv");
14235 std::fs::write(&csv, "name,age\nAlice,30\n").unwrap();
14236 let src = format!(
14237 r#"let t = read_csv("{}")
14238let r = t |> filter(age > 0) |> collect()
14239print(t)"#,
14240 csv.to_str().unwrap()
14241 );
14242 let result = run(&src);
14243 assert!(result.is_err(), "Should error on use-after-move");
14244 }
14245
14246 #[test]
14247 fn test_ir_non_table_op_fallback() {
14248 let output = run_output(
14250 r#"
14251fn double(x) { x * 2 }
14252let result = 5 |> double()
14253print(result)
14254"#,
14255 );
14256 assert_eq!(output, vec!["10"]);
14257 }
14258
14259 #[test]
14260 fn test_ir_mixed_pipe_fallback() {
14261 let output = run_output(
14263 r#"
14264let result = [3, 1, 2] |> len()
14265print(result)
14266"#,
14267 );
14268 assert_eq!(output, vec!["3"]);
14269 }
14270
14271 #[test]
14272 fn test_ir_single_filter_roundtrip() {
14273 let dir = tempfile::tempdir().unwrap();
14275 let csv = dir.path().join("data.csv");
14276 std::fs::write(&csv, "name,age\nAlice,30\nBob,20\n").unwrap();
14277 let src = format!(
14278 r#"let t = read_csv("{}")
14279let r = t |> filter(age > 25) |> collect()
14280print(r)"#,
14281 csv.to_str().unwrap()
14282 );
14283 let output = run_output(&src);
14284 assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14285 assert!(!output[0].contains("Bob"), "Output: {}", output[0]);
14286 }
14287
14288 #[test]
14291 fn test_vm_agent_definition() {
14292 let output = run_output(
14293 r#"
14294fn search(query) { "found: " + query }
14295agent bot {
14296 model: "gpt-4o",
14297 system: "You are helpful.",
14298 tools {
14299 search: {
14300 description: "Search the web",
14301 parameters: {}
14302 }
14303 },
14304 max_turns: 5
14305}
14306print(type_of(bot))
14307print(bot)
14308"#,
14309 );
14310 assert_eq!(output, vec!["agent", "<agent bot>"]);
14311 }
14312
14313 #[test]
14314 fn test_vm_agent_minimal() {
14315 let output = run_output(
14316 r#"
14317agent minimal_bot {
14318 model: "claude-sonnet-4-20250514"
14319}
14320print(type_of(minimal_bot))
14321"#,
14322 );
14323 assert_eq!(output, vec!["agent"]);
14324 }
14325
14326 #[test]
14327 fn test_vm_agent_with_base_url() {
14328 let output = run_output(
14329 r#"
14330agent local_bot {
14331 model: "llama3",
14332 base_url: "http://localhost:11434/v1",
14333 max_turns: 3
14334}
14335print(local_bot)
14336"#,
14337 );
14338 assert_eq!(output, vec!["<agent local_bot>"]);
14339 }
14340
14341 #[test]
14342 fn test_vm_agent_multiple_tools() {
14343 let output = run_output(
14344 r#"
14345fn search(query) { "result" }
14346fn weather(city) { "sunny" }
14347agent helper {
14348 model: "gpt-4o",
14349 tools {
14350 search: { description: "Search", parameters: {} },
14351 weather: { description: "Get weather", parameters: {} }
14352 }
14353}
14354print(type_of(helper))
14355"#,
14356 );
14357 assert_eq!(output, vec!["agent"]);
14358 }
14359
14360 #[test]
14361 fn test_vm_agent_lifecycle_hooks_stored() {
14362 let output = run_output(
14363 r#"
14364fn search(q) { "result" }
14365agent bot {
14366 model: "gpt-4o",
14367 tools {
14368 search: { description: "Search", parameters: {} }
14369 },
14370 on_tool_call {
14371 println("tool: " + tool_name)
14372 }
14373 on_complete {
14374 println("done")
14375 }
14376}
14377print(type_of(bot))
14378print(type_of(__agent_bot_on_tool_call__))
14379print(type_of(__agent_bot_on_complete__))
14380"#,
14381 );
14382 assert_eq!(output, vec!["agent", "function", "function"]);
14383 }
14384
14385 #[test]
14386 fn test_vm_agent_lifecycle_hook_callable() {
14387 let output = run_output(
14388 r#"
14389agent bot {
14390 model: "gpt-4o",
14391 on_tool_call {
14392 println("called: " + tool_name + " -> " + tool_result)
14393 }
14394 on_complete {
14395 println("completed")
14396 }
14397}
14398__agent_bot_on_tool_call__("search", "query", "found it")
14399__agent_bot_on_complete__("hello")
14400"#,
14401 );
14402 assert_eq!(output, vec!["called: search -> found it", "completed"]);
14403 }
14404}