1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::io::BufRead;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, Ordering};
6use std::sync::Arc;
7
8use crate::value::{values_equal, VmAtomicHandle, VmChannelHandle, VmError, VmValue};
9use crate::vm::Vm;
10
11thread_local! {
13 static REGEX_CACHE: RefCell<Vec<(String, regex::Regex)>> = const { RefCell::new(Vec::new()) };
14}
15
16fn get_cached_regex(pattern: &str) -> Result<regex::Regex, VmError> {
17 REGEX_CACHE.with(|cache| {
18 let mut cache = cache.borrow_mut();
19 if let Some(pos) = cache.iter().position(|(p, _)| p == pattern) {
21 return Ok(cache[pos].1.clone());
22 }
23 let re = regex::Regex::new(pattern).map_err(|e| {
25 VmError::Thrown(VmValue::String(Rc::from(format!("Invalid regex: {e}"))))
26 })?;
27 cache.push((pattern.to_string(), re.clone()));
28 if cache.len() > 64 {
30 cache.remove(0);
31 }
32 Ok(re)
33 })
34}
35
36use crate::http::register_http_builtins;
37use crate::llm::register_llm_builtins;
38use crate::mcp::register_mcp_builtins;
39
40fn select_result(index: usize, value: VmValue, channel_name: &str) -> VmValue {
42 let mut result = BTreeMap::new();
43 result.insert("index".to_string(), VmValue::Int(index as i64));
44 result.insert("value".to_string(), value);
45 result.insert(
46 "channel".to_string(),
47 VmValue::String(Rc::from(channel_name)),
48 );
49 VmValue::Dict(Rc::new(result))
50}
51
52fn select_none() -> VmValue {
54 let mut result = BTreeMap::new();
55 result.insert("index".to_string(), VmValue::Int(-1));
56 result.insert("value".to_string(), VmValue::Nil);
57 result.insert("channel".to_string(), VmValue::Nil);
58 VmValue::Dict(Rc::new(result))
59}
60
61fn try_poll_channels(channels: &[VmValue]) -> (Option<(usize, VmValue, String)>, bool) {
65 let mut all_closed = true;
66 for (i, ch_val) in channels.iter().enumerate() {
67 if let VmValue::Channel(ch) = ch_val {
68 if let Ok(mut rx) = ch.receiver.try_lock() {
69 match rx.try_recv() {
70 Ok(val) => return (Some((i, val, ch.name.clone())), false),
71 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
72 all_closed = false;
73 }
74 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {}
75 }
76 } else {
77 all_closed = false;
78 }
79 }
80 }
81 (None, all_closed)
82}
83
84pub fn register_vm_stdlib(vm: &mut Vm) {
86 vm.register_builtin("log", |args, out| {
87 let msg = args.first().map(|a| a.display()).unwrap_or_default();
88 out.push_str(&format!("[harn] {msg}\n"));
89 Ok(VmValue::Nil)
90 });
91 vm.register_builtin("print", |args, out| {
92 let msg = args.first().map(|a| a.display()).unwrap_or_default();
93 out.push_str(&msg);
94 Ok(VmValue::Nil)
95 });
96 vm.register_builtin("println", |args, out| {
97 let msg = args.first().map(|a| a.display()).unwrap_or_default();
98 out.push_str(&format!("{msg}\n"));
99 Ok(VmValue::Nil)
100 });
101 vm.register_builtin("type_of", |args, _out| {
102 let val = args.first().unwrap_or(&VmValue::Nil);
103 Ok(VmValue::String(Rc::from(val.type_name())))
104 });
105 vm.register_builtin("to_string", |args, _out| {
106 let val = args.first().unwrap_or(&VmValue::Nil);
107 Ok(VmValue::String(Rc::from(val.display())))
108 });
109 vm.register_builtin("to_int", |args, _out| {
110 let val = args.first().unwrap_or(&VmValue::Nil);
111 match val {
112 VmValue::Int(n) => Ok(VmValue::Int(*n)),
113 VmValue::Float(n) => Ok(VmValue::Int(*n as i64)),
114 VmValue::String(s) => Ok(s.parse::<i64>().map(VmValue::Int).unwrap_or(VmValue::Nil)),
115 _ => Ok(VmValue::Nil),
116 }
117 });
118 vm.register_builtin("to_float", |args, _out| {
119 let val = args.first().unwrap_or(&VmValue::Nil);
120 match val {
121 VmValue::Float(n) => Ok(VmValue::Float(*n)),
122 VmValue::Int(n) => Ok(VmValue::Float(*n as f64)),
123 VmValue::String(s) => Ok(s.parse::<f64>().map(VmValue::Float).unwrap_or(VmValue::Nil)),
124 _ => Ok(VmValue::Nil),
125 }
126 });
127
128 vm.register_builtin("Ok", |args, _out| {
130 let val = args.first().cloned().unwrap_or(VmValue::Nil);
131 Ok(VmValue::EnumVariant {
132 enum_name: "Result".into(),
133 variant: "Ok".into(),
134 fields: vec![val],
135 })
136 });
137 vm.register_builtin("Err", |args, _out| {
138 let val = args.first().cloned().unwrap_or(VmValue::Nil);
139 Ok(VmValue::EnumVariant {
140 enum_name: "Result".into(),
141 variant: "Err".into(),
142 fields: vec![val],
143 })
144 });
145 vm.register_builtin("is_ok", |args, _out| {
146 let val = args.first().unwrap_or(&VmValue::Nil);
147 Ok(VmValue::Bool(matches!(
148 val,
149 VmValue::EnumVariant { enum_name, variant, .. }
150 if enum_name == "Result" && variant == "Ok"
151 )))
152 });
153 vm.register_builtin("is_err", |args, _out| {
154 let val = args.first().unwrap_or(&VmValue::Nil);
155 Ok(VmValue::Bool(matches!(
156 val,
157 VmValue::EnumVariant { enum_name, variant, .. }
158 if enum_name == "Result" && variant == "Err"
159 )))
160 });
161 vm.register_builtin("unwrap", |args, _out| {
162 let val = args.first().unwrap_or(&VmValue::Nil);
163 match val {
164 VmValue::EnumVariant {
165 enum_name,
166 variant,
167 fields,
168 } if enum_name == "Result" && variant == "Ok" => {
169 Ok(fields.first().cloned().unwrap_or(VmValue::Nil))
170 }
171 VmValue::EnumVariant {
172 enum_name,
173 variant,
174 fields,
175 } if enum_name == "Result" && variant == "Err" => {
176 let msg = fields.first().map(|f| f.display()).unwrap_or_default();
177 Err(VmError::Runtime(format!("unwrap called on Err: {msg}")))
178 }
179 _ => Ok(val.clone()),
180 }
181 });
182 vm.register_builtin("unwrap_or", |args, _out| {
183 let val = args.first().unwrap_or(&VmValue::Nil);
184 let default = args.get(1).cloned().unwrap_or(VmValue::Nil);
185 match val {
186 VmValue::EnumVariant {
187 enum_name,
188 variant,
189 fields,
190 } if enum_name == "Result" && variant == "Ok" => {
191 Ok(fields.first().cloned().unwrap_or(VmValue::Nil))
192 }
193 VmValue::EnumVariant {
194 enum_name, variant, ..
195 } if enum_name == "Result" && variant == "Err" => Ok(default),
196 _ => Ok(val.clone()),
197 }
198 });
199 vm.register_builtin("unwrap_err", |args, _out| {
200 let val = args.first().unwrap_or(&VmValue::Nil);
201 match val {
202 VmValue::EnumVariant {
203 enum_name,
204 variant,
205 fields,
206 } if enum_name == "Result" && variant == "Err" => {
207 Ok(fields.first().cloned().unwrap_or(VmValue::Nil))
208 }
209 _ => Err(VmError::Runtime("unwrap_err called on non-Err".into())),
210 }
211 });
212
213 vm.register_builtin("json_stringify", |args, _out| {
214 let val = args.first().unwrap_or(&VmValue::Nil);
215 Ok(VmValue::String(Rc::from(vm_value_to_json(val))))
216 });
217
218 vm.register_builtin("json_parse", |args, _out| {
219 let text = args.first().map(|a| a.display()).unwrap_or_default();
220 match serde_json::from_str::<serde_json::Value>(&text) {
221 Ok(jv) => Ok(json_to_vm_value(&jv)),
222 Err(e) => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
223 "JSON parse error: {e}"
224 ))))),
225 }
226 });
227
228 vm.register_builtin("env", |args, _out| {
229 let name = args.first().map(|a| a.display()).unwrap_or_default();
230 match std::env::var(&name) {
231 Ok(val) => Ok(VmValue::String(Rc::from(val))),
232 Err(_) => Ok(VmValue::Nil),
233 }
234 });
235
236 vm.register_builtin("timestamp", |_args, _out| {
237 use std::time::{SystemTime, UNIX_EPOCH};
238 let secs = SystemTime::now()
239 .duration_since(UNIX_EPOCH)
240 .map(|d| d.as_secs_f64())
241 .unwrap_or(0.0);
242 Ok(VmValue::Float(secs))
243 });
244
245 vm.register_builtin("read_file", |args, _out| {
246 let path = args.first().map(|a| a.display()).unwrap_or_default();
247 match std::fs::read_to_string(&path) {
248 Ok(content) => Ok(VmValue::String(Rc::from(content))),
249 Err(e) => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
250 "Failed to read file {path}: {e}"
251 ))))),
252 }
253 });
254
255 vm.register_builtin("write_file", |args, _out| {
256 if args.len() >= 2 {
257 let path = args[0].display();
258 let content = args[1].display();
259 std::fs::write(&path, &content).map_err(|e| {
260 VmError::Thrown(VmValue::String(Rc::from(format!(
261 "Failed to write file {path}: {e}"
262 ))))
263 })?;
264 }
265 Ok(VmValue::Nil)
266 });
267
268 vm.register_builtin("exit", |args, _out| {
269 let code = args.first().and_then(|a| a.as_int()).unwrap_or(0);
270 std::process::exit(code as i32);
271 });
272
273 vm.register_builtin("regex_match", |args, _out| {
274 if args.len() >= 2 {
275 let pattern = args[0].display();
276 let text = args[1].display();
277 let re = get_cached_regex(&pattern)?;
278 let matches: Vec<VmValue> = re
279 .find_iter(&text)
280 .map(|m| VmValue::String(Rc::from(m.as_str())))
281 .collect();
282 if matches.is_empty() {
283 return Ok(VmValue::Nil);
284 }
285 return Ok(VmValue::List(Rc::new(matches)));
286 }
287 Ok(VmValue::Nil)
288 });
289
290 vm.register_builtin("regex_replace", |args, _out| {
291 if args.len() >= 3 {
292 let pattern = args[0].display();
293 let replacement = args[1].display();
294 let text = args[2].display();
295 let re = get_cached_regex(&pattern)?;
296 return Ok(VmValue::String(Rc::from(
297 re.replace_all(&text, replacement.as_str()).into_owned(),
298 )));
299 }
300 Ok(VmValue::Nil)
301 });
302
303 vm.register_builtin("regex_captures", |args, _out| {
304 if args.len() < 2 {
305 return Ok(VmValue::List(Rc::new(Vec::new())));
306 }
307 let pattern = args[0].display();
308 let text = args[1].display();
309 let re = get_cached_regex(&pattern)?;
310
311 let mut results: Vec<VmValue> = Vec::new();
312 for caps in re.captures_iter(&text) {
313 let mut dict = BTreeMap::new();
314
315 dict.insert(
317 "match".to_string(),
318 VmValue::String(Rc::from(caps.get(0).map_or("", |m| m.as_str()))),
319 );
320
321 let groups: Vec<VmValue> = (1..caps.len())
323 .map(|i| match caps.get(i) {
324 Some(m) => VmValue::String(Rc::from(m.as_str())),
325 None => VmValue::Nil,
326 })
327 .collect();
328 dict.insert("groups".to_string(), VmValue::List(Rc::new(groups)));
329
330 for name in re.capture_names().flatten() {
332 if let Some(m) = caps.name(name) {
333 dict.insert(name.to_string(), VmValue::String(Rc::from(m.as_str())));
334 }
335 }
336
337 results.push(VmValue::Dict(Rc::new(dict)));
338 }
339 Ok(VmValue::List(Rc::new(results)))
340 });
341
342 vm.register_builtin("__make_struct", |args, _out| {
344 let struct_name = args.first().map(|a| a.display()).unwrap_or_default();
345 let fields_dict = args.get(1).cloned().unwrap_or(VmValue::Nil);
346 match fields_dict {
347 VmValue::Dict(d) => Ok(VmValue::StructInstance {
348 struct_name,
349 fields: (*d).clone(),
350 }),
351 _ => Ok(VmValue::StructInstance {
352 struct_name,
353 fields: BTreeMap::new(),
354 }),
355 }
356 });
357
358 vm.register_builtin("base64_encode", |args, _out| {
360 let val = args.first().map(|a| a.display()).unwrap_or_default();
361 use base64::Engine;
362 Ok(VmValue::String(Rc::from(
363 base64::engine::general_purpose::STANDARD.encode(val.as_bytes()),
364 )))
365 });
366 vm.register_builtin("base64_decode", |args, _out| {
367 let val = args.first().map(|a| a.display()).unwrap_or_default();
368 use base64::Engine;
369 match base64::engine::general_purpose::STANDARD.decode(val.as_bytes()) {
370 Ok(bytes) => Ok(VmValue::String(Rc::from(
371 String::from_utf8_lossy(&bytes).into_owned(),
372 ))),
373 Err(e) => Err(VmError::Runtime(format!("base64 decode error: {e}"))),
374 }
375 });
376
377 vm.register_builtin("sha256", |args, _out| {
379 use sha2::Digest;
380 let val = args.first().map(|a| a.display()).unwrap_or_default();
381 let hash = sha2::Sha256::digest(val.as_bytes());
382 Ok(VmValue::String(Rc::from(format!("{hash:x}"))))
383 });
384 vm.register_builtin("md5", |args, _out| {
385 use md5::Digest;
386 let val = args.first().map(|a| a.display()).unwrap_or_default();
387 let hash = md5::Md5::digest(val.as_bytes());
388 Ok(VmValue::String(Rc::from(format!("{hash:x}"))))
389 });
390
391 vm.register_builtin("prompt_user", |args, out| {
392 let msg = args.first().map(|a| a.display()).unwrap_or_default();
393 out.push_str(&msg);
394 let mut input = String::new();
395 if std::io::stdin().lock().read_line(&mut input).is_ok() {
396 Ok(VmValue::String(Rc::from(input.trim_end())))
397 } else {
398 Ok(VmValue::Nil)
399 }
400 });
401
402 vm.register_builtin("abs", |args, _out| {
405 match args.first().unwrap_or(&VmValue::Nil) {
406 VmValue::Int(n) => Ok(VmValue::Int(n.wrapping_abs())),
407 VmValue::Float(n) => Ok(VmValue::Float(n.abs())),
408 _ => Ok(VmValue::Nil),
409 }
410 });
411
412 vm.register_builtin("min", |args, _out| {
413 if args.len() >= 2 {
414 match (&args[0], &args[1]) {
415 (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.min(y))),
416 (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.min(*y))),
417 (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).min(*y))),
418 (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.min(*y as f64))),
419 _ => Ok(VmValue::Nil),
420 }
421 } else {
422 Ok(VmValue::Nil)
423 }
424 });
425
426 vm.register_builtin("max", |args, _out| {
427 if args.len() >= 2 {
428 match (&args[0], &args[1]) {
429 (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.max(y))),
430 (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.max(*y))),
431 (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).max(*y))),
432 (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.max(*y as f64))),
433 _ => Ok(VmValue::Nil),
434 }
435 } else {
436 Ok(VmValue::Nil)
437 }
438 });
439
440 vm.register_builtin("floor", |args, _out| {
441 match args.first().unwrap_or(&VmValue::Nil) {
442 VmValue::Float(n) => Ok(VmValue::Int(n.floor() as i64)),
443 VmValue::Int(n) => Ok(VmValue::Int(*n)),
444 _ => Ok(VmValue::Nil),
445 }
446 });
447
448 vm.register_builtin("ceil", |args, _out| {
449 match args.first().unwrap_or(&VmValue::Nil) {
450 VmValue::Float(n) => Ok(VmValue::Int(n.ceil() as i64)),
451 VmValue::Int(n) => Ok(VmValue::Int(*n)),
452 _ => Ok(VmValue::Nil),
453 }
454 });
455
456 vm.register_builtin("round", |args, _out| {
457 match args.first().unwrap_or(&VmValue::Nil) {
458 VmValue::Float(n) => Ok(VmValue::Int(n.round() as i64)),
459 VmValue::Int(n) => Ok(VmValue::Int(*n)),
460 _ => Ok(VmValue::Nil),
461 }
462 });
463
464 vm.register_builtin("sqrt", |args, _out| {
465 match args.first().unwrap_or(&VmValue::Nil) {
466 VmValue::Float(n) => Ok(VmValue::Float(n.sqrt())),
467 VmValue::Int(n) => Ok(VmValue::Float((*n as f64).sqrt())),
468 _ => Ok(VmValue::Nil),
469 }
470 });
471
472 vm.register_builtin("pow", |args, _out| {
473 if args.len() >= 2 {
474 match (&args[0], &args[1]) {
475 (VmValue::Int(base), VmValue::Int(exp)) => {
476 if *exp >= 0 && *exp <= u32::MAX as i64 {
477 Ok(VmValue::Int(base.wrapping_pow(*exp as u32)))
478 } else {
479 Ok(VmValue::Float((*base as f64).powf(*exp as f64)))
480 }
481 }
482 (VmValue::Float(base), VmValue::Int(exp)) => {
483 if *exp >= i32::MIN as i64 && *exp <= i32::MAX as i64 {
484 Ok(VmValue::Float(base.powi(*exp as i32)))
485 } else {
486 Ok(VmValue::Float(base.powf(*exp as f64)))
487 }
488 }
489 (VmValue::Int(base), VmValue::Float(exp)) => {
490 Ok(VmValue::Float((*base as f64).powf(*exp)))
491 }
492 (VmValue::Float(base), VmValue::Float(exp)) => Ok(VmValue::Float(base.powf(*exp))),
493 _ => Ok(VmValue::Nil),
494 }
495 } else {
496 Ok(VmValue::Nil)
497 }
498 });
499
500 vm.register_builtin("random", |_args, _out| {
501 use rand::Rng;
502 let val: f64 = rand::thread_rng().gen();
503 Ok(VmValue::Float(val))
504 });
505
506 vm.register_builtin("random_int", |args, _out| {
507 use rand::Rng;
508 if args.len() >= 2 {
509 let min = args[0].as_int().unwrap_or(0);
510 let max = args[1].as_int().unwrap_or(0);
511 if min <= max {
512 let val = rand::thread_rng().gen_range(min..=max);
513 return Ok(VmValue::Int(val));
514 }
515 }
516 Ok(VmValue::Nil)
517 });
518
519 vm.register_builtin("sin", |args, _out| {
522 let n = match args.first().unwrap_or(&VmValue::Nil) {
523 VmValue::Float(n) => *n,
524 VmValue::Int(n) => *n as f64,
525 _ => return Ok(VmValue::Nil),
526 };
527 Ok(VmValue::Float(n.sin()))
528 });
529
530 vm.register_builtin("cos", |args, _out| {
531 let n = match args.first().unwrap_or(&VmValue::Nil) {
532 VmValue::Float(n) => *n,
533 VmValue::Int(n) => *n as f64,
534 _ => return Ok(VmValue::Nil),
535 };
536 Ok(VmValue::Float(n.cos()))
537 });
538
539 vm.register_builtin("tan", |args, _out| {
540 let n = match args.first().unwrap_or(&VmValue::Nil) {
541 VmValue::Float(n) => *n,
542 VmValue::Int(n) => *n as f64,
543 _ => return Ok(VmValue::Nil),
544 };
545 Ok(VmValue::Float(n.tan()))
546 });
547
548 vm.register_builtin("asin", |args, _out| {
549 let n = match args.first().unwrap_or(&VmValue::Nil) {
550 VmValue::Float(n) => *n,
551 VmValue::Int(n) => *n as f64,
552 _ => return Ok(VmValue::Nil),
553 };
554 Ok(VmValue::Float(n.asin()))
555 });
556
557 vm.register_builtin("acos", |args, _out| {
558 let n = match args.first().unwrap_or(&VmValue::Nil) {
559 VmValue::Float(n) => *n,
560 VmValue::Int(n) => *n as f64,
561 _ => return Ok(VmValue::Nil),
562 };
563 Ok(VmValue::Float(n.acos()))
564 });
565
566 vm.register_builtin("atan", |args, _out| {
567 let n = match args.first().unwrap_or(&VmValue::Nil) {
568 VmValue::Float(n) => *n,
569 VmValue::Int(n) => *n as f64,
570 _ => return Ok(VmValue::Nil),
571 };
572 Ok(VmValue::Float(n.atan()))
573 });
574
575 vm.register_builtin("atan2", |args, _out| {
576 if args.len() >= 2 {
577 let y = match &args[0] {
578 VmValue::Float(n) => *n,
579 VmValue::Int(n) => *n as f64,
580 _ => return Ok(VmValue::Nil),
581 };
582 let x = match &args[1] {
583 VmValue::Float(n) => *n,
584 VmValue::Int(n) => *n as f64,
585 _ => return Ok(VmValue::Nil),
586 };
587 Ok(VmValue::Float(y.atan2(x)))
588 } else {
589 Ok(VmValue::Nil)
590 }
591 });
592
593 vm.register_builtin("log2", |args, _out| {
594 let n = match args.first().unwrap_or(&VmValue::Nil) {
595 VmValue::Float(n) => *n,
596 VmValue::Int(n) => *n as f64,
597 _ => return Ok(VmValue::Nil),
598 };
599 Ok(VmValue::Float(n.log2()))
600 });
601
602 vm.register_builtin("log10", |args, _out| {
603 let n = match args.first().unwrap_or(&VmValue::Nil) {
604 VmValue::Float(n) => *n,
605 VmValue::Int(n) => *n as f64,
606 _ => return Ok(VmValue::Nil),
607 };
608 Ok(VmValue::Float(n.log10()))
609 });
610
611 vm.register_builtin("ln", |args, _out| {
612 let n = match args.first().unwrap_or(&VmValue::Nil) {
613 VmValue::Float(n) => *n,
614 VmValue::Int(n) => *n as f64,
615 _ => return Ok(VmValue::Nil),
616 };
617 Ok(VmValue::Float(n.ln()))
618 });
619
620 vm.register_builtin("exp", |args, _out| {
621 let n = match args.first().unwrap_or(&VmValue::Nil) {
622 VmValue::Float(n) => *n,
623 VmValue::Int(n) => *n as f64,
624 _ => return Ok(VmValue::Nil),
625 };
626 Ok(VmValue::Float(n.exp()))
627 });
628
629 vm.register_builtin("sign", |args, _out| {
630 match args.first().unwrap_or(&VmValue::Nil) {
631 VmValue::Int(n) => Ok(VmValue::Int(n.signum())),
632 VmValue::Float(n) => {
633 if n.is_nan() {
634 Ok(VmValue::Float(f64::NAN))
635 } else if *n == 0.0 {
636 Ok(VmValue::Int(0))
637 } else if *n > 0.0 {
638 Ok(VmValue::Int(1))
639 } else {
640 Ok(VmValue::Int(-1))
641 }
642 }
643 _ => Ok(VmValue::Nil),
644 }
645 });
646
647 vm.register_builtin("pi", |_args, _out| Ok(VmValue::Float(std::f64::consts::PI)));
648
649 vm.register_builtin("e", |_args, _out| Ok(VmValue::Float(std::f64::consts::E)));
650
651 vm.register_builtin("is_nan", |args, _out| {
652 match args.first().unwrap_or(&VmValue::Nil) {
653 VmValue::Float(n) => Ok(VmValue::Bool(n.is_nan())),
654 _ => Ok(VmValue::Bool(false)),
655 }
656 });
657
658 vm.register_builtin("is_infinite", |args, _out| {
659 match args.first().unwrap_or(&VmValue::Nil) {
660 VmValue::Float(n) => Ok(VmValue::Bool(n.is_infinite())),
661 _ => Ok(VmValue::Bool(false)),
662 }
663 });
664
665 vm.register_builtin("set", |args, _out| {
669 let mut items: Vec<VmValue> = Vec::new();
670 for arg in args {
671 match arg {
672 VmValue::List(list) => {
673 for v in list.iter() {
674 if !items.iter().any(|x| values_equal(x, v)) {
675 items.push(v.clone());
676 }
677 }
678 }
679 VmValue::Set(s) => {
680 for v in s.iter() {
681 if !items.iter().any(|x| values_equal(x, v)) {
682 items.push(v.clone());
683 }
684 }
685 }
686 other => {
687 if !items.iter().any(|x| values_equal(x, other)) {
688 items.push(other.clone());
689 }
690 }
691 }
692 }
693 Ok(VmValue::Set(Rc::new(items)))
694 });
695
696 vm.register_builtin("set_add", |args, _out| {
698 let s = match args.first() {
699 Some(VmValue::Set(s)) => s.clone(),
700 _ => {
701 return Err(VmError::Thrown(VmValue::String(Rc::from(
702 "set_add: first argument must be a set",
703 ))));
704 }
705 };
706 let val = args.get(1).cloned().unwrap_or(VmValue::Nil);
707 let mut items: Vec<VmValue> = (*s).clone();
708 if !items.iter().any(|x| values_equal(x, &val)) {
709 items.push(val);
710 }
711 Ok(VmValue::Set(Rc::new(items)))
712 });
713
714 vm.register_builtin("set_remove", |args, _out| {
716 let s = match args.first() {
717 Some(VmValue::Set(s)) => s.clone(),
718 _ => {
719 return Err(VmError::Thrown(VmValue::String(Rc::from(
720 "set_remove: first argument must be a set",
721 ))));
722 }
723 };
724 let val = args.get(1).cloned().unwrap_or(VmValue::Nil);
725 let items: Vec<VmValue> = s
726 .iter()
727 .filter(|x| !values_equal(x, &val))
728 .cloned()
729 .collect();
730 Ok(VmValue::Set(Rc::new(items)))
731 });
732
733 vm.register_builtin("set_contains", |args, _out| {
735 let s = match args.first() {
736 Some(VmValue::Set(s)) => s,
737 _ => return Ok(VmValue::Bool(false)),
738 };
739 let val = args.get(1).unwrap_or(&VmValue::Nil);
740 Ok(VmValue::Bool(s.iter().any(|x| values_equal(x, val))))
741 });
742
743 vm.register_builtin("set_union", |args, _out| {
745 let a = match args.first() {
746 Some(VmValue::Set(s)) => s,
747 _ => {
748 return Err(VmError::Thrown(VmValue::String(Rc::from(
749 "set_union: arguments must be sets",
750 ))));
751 }
752 };
753 let b = match args.get(1) {
754 Some(VmValue::Set(s)) => s,
755 _ => {
756 return Err(VmError::Thrown(VmValue::String(Rc::from(
757 "set_union: arguments must be sets",
758 ))));
759 }
760 };
761 let mut items: Vec<VmValue> = (**a).clone();
762 for v in b.iter() {
763 if !items.iter().any(|x| values_equal(x, v)) {
764 items.push(v.clone());
765 }
766 }
767 Ok(VmValue::Set(Rc::new(items)))
768 });
769
770 vm.register_builtin("set_intersect", |args, _out| {
772 let a = match args.first() {
773 Some(VmValue::Set(s)) => s,
774 _ => {
775 return Err(VmError::Thrown(VmValue::String(Rc::from(
776 "set_intersect: arguments must be sets",
777 ))));
778 }
779 };
780 let b = match args.get(1) {
781 Some(VmValue::Set(s)) => s,
782 _ => {
783 return Err(VmError::Thrown(VmValue::String(Rc::from(
784 "set_intersect: arguments must be sets",
785 ))));
786 }
787 };
788 let items: Vec<VmValue> = a
789 .iter()
790 .filter(|x| b.iter().any(|y| values_equal(x, y)))
791 .cloned()
792 .collect();
793 Ok(VmValue::Set(Rc::new(items)))
794 });
795
796 vm.register_builtin("set_difference", |args, _out| {
798 let a = match args.first() {
799 Some(VmValue::Set(s)) => s,
800 _ => {
801 return Err(VmError::Thrown(VmValue::String(Rc::from(
802 "set_difference: arguments must be sets",
803 ))));
804 }
805 };
806 let b = match args.get(1) {
807 Some(VmValue::Set(s)) => s,
808 _ => {
809 return Err(VmError::Thrown(VmValue::String(Rc::from(
810 "set_difference: arguments must be sets",
811 ))));
812 }
813 };
814 let items: Vec<VmValue> = a
815 .iter()
816 .filter(|x| !b.iter().any(|y| values_equal(x, y)))
817 .cloned()
818 .collect();
819 Ok(VmValue::Set(Rc::new(items)))
820 });
821
822 vm.register_builtin("to_list", |args, _out| {
824 match args.first().unwrap_or(&VmValue::Nil) {
825 VmValue::Set(s) => Ok(VmValue::List(Rc::new(s.to_vec()))),
826 VmValue::List(l) => Ok(VmValue::List(l.clone())),
827 other => Ok(VmValue::List(Rc::new(vec![other.clone()]))),
828 }
829 });
830
831 vm.register_builtin("assert", |args, _out| {
834 let condition = args.first().unwrap_or(&VmValue::Nil);
835 if !condition.is_truthy() {
836 let msg = args
837 .get(1)
838 .map(|a| a.display())
839 .unwrap_or_else(|| "Assertion failed".to_string());
840 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
841 }
842 Ok(VmValue::Nil)
843 });
844
845 vm.register_builtin("assert_eq", |args, _out| {
846 if args.len() >= 2 {
847 if !values_equal(&args[0], &args[1]) {
848 let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
849 format!(
850 "Assertion failed: expected {}, got {}",
851 args[1].display(),
852 args[0].display()
853 )
854 });
855 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
856 }
857 Ok(VmValue::Nil)
858 } else {
859 Err(VmError::Thrown(VmValue::String(Rc::from(
860 "assert_eq requires at least 2 arguments",
861 ))))
862 }
863 });
864
865 vm.register_builtin("assert_ne", |args, _out| {
866 if args.len() >= 2 {
867 if values_equal(&args[0], &args[1]) {
868 let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
869 format!(
870 "Assertion failed: values should not be equal: {}",
871 args[0].display()
872 )
873 });
874 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
875 }
876 Ok(VmValue::Nil)
877 } else {
878 Err(VmError::Thrown(VmValue::String(Rc::from(
879 "assert_ne requires at least 2 arguments",
880 ))))
881 }
882 });
883
884 vm.register_builtin("__range__", |args, _out| {
885 let start = args.first().and_then(|a| a.as_int()).unwrap_or(0);
886 let end = args.get(1).and_then(|a| a.as_int()).unwrap_or(0);
887 let inclusive = args.get(2).map(|a| a.is_truthy()).unwrap_or(false);
888 let items: Vec<VmValue> = if inclusive {
889 (start..=end).map(VmValue::Int).collect()
890 } else {
891 (start..end).map(VmValue::Int).collect()
892 };
893 Ok(VmValue::List(Rc::new(items)))
894 });
895
896 vm.register_builtin("file_exists", |args, _out| {
901 let path = args.first().map(|a| a.display()).unwrap_or_default();
902 Ok(VmValue::Bool(std::path::Path::new(&path).exists()))
903 });
904
905 vm.register_builtin("delete_file", |args, _out| {
906 let path = args.first().map(|a| a.display()).unwrap_or_default();
907 let p = std::path::Path::new(&path);
908 if p.is_dir() {
909 std::fs::remove_dir_all(&path).map_err(|e| {
910 VmError::Thrown(VmValue::String(Rc::from(format!(
911 "Failed to delete directory {path}: {e}"
912 ))))
913 })?;
914 } else {
915 std::fs::remove_file(&path).map_err(|e| {
916 VmError::Thrown(VmValue::String(Rc::from(format!(
917 "Failed to delete file {path}: {e}"
918 ))))
919 })?;
920 }
921 Ok(VmValue::Nil)
922 });
923
924 vm.register_builtin("append_file", |args, _out| {
925 use std::io::Write;
926 if args.len() >= 2 {
927 let path = args[0].display();
928 let content = args[1].display();
929 let mut file = std::fs::OpenOptions::new()
930 .append(true)
931 .create(true)
932 .open(&path)
933 .map_err(|e| {
934 VmError::Thrown(VmValue::String(Rc::from(format!(
935 "Failed to open file {path}: {e}"
936 ))))
937 })?;
938 file.write_all(content.as_bytes()).map_err(|e| {
939 VmError::Thrown(VmValue::String(Rc::from(format!(
940 "Failed to append to file {path}: {e}"
941 ))))
942 })?;
943 }
944 Ok(VmValue::Nil)
945 });
946
947 vm.register_builtin("list_dir", |args, _out| {
948 let path = args
949 .first()
950 .map(|a| a.display())
951 .unwrap_or_else(|| ".".to_string());
952 let entries = std::fs::read_dir(&path).map_err(|e| {
953 VmError::Thrown(VmValue::String(Rc::from(format!(
954 "Failed to list directory {path}: {e}"
955 ))))
956 })?;
957 let mut result = Vec::new();
958 for entry in entries {
959 let entry =
960 entry.map_err(|e| VmError::Thrown(VmValue::String(Rc::from(e.to_string()))))?;
961 let name = entry.file_name().to_string_lossy().to_string();
962 result.push(VmValue::String(Rc::from(name.as_str())));
963 }
964 result.sort_by_key(|a| a.display());
965 Ok(VmValue::List(Rc::new(result)))
966 });
967
968 vm.register_builtin("mkdir", |args, _out| {
969 let path = args.first().map(|a| a.display()).unwrap_or_default();
970 std::fs::create_dir_all(&path).map_err(|e| {
971 VmError::Thrown(VmValue::String(Rc::from(format!(
972 "Failed to create directory {path}: {e}"
973 ))))
974 })?;
975 Ok(VmValue::Nil)
976 });
977
978 vm.register_builtin("path_join", |args, _out| {
979 let mut path = std::path::PathBuf::new();
980 for arg in args {
981 path.push(arg.display());
982 }
983 Ok(VmValue::String(Rc::from(
984 path.to_string_lossy().to_string().as_str(),
985 )))
986 });
987
988 vm.register_builtin("copy_file", |args, _out| {
989 if args.len() >= 2 {
990 let src = args[0].display();
991 let dst = args[1].display();
992 std::fs::copy(&src, &dst).map_err(|e| {
993 VmError::Thrown(VmValue::String(Rc::from(format!(
994 "Failed to copy {src} to {dst}: {e}"
995 ))))
996 })?;
997 }
998 Ok(VmValue::Nil)
999 });
1000
1001 vm.register_builtin("temp_dir", |_args, _out| {
1002 Ok(VmValue::String(Rc::from(
1003 std::env::temp_dir().to_string_lossy().to_string().as_str(),
1004 )))
1005 });
1006
1007 vm.register_builtin("stat", |args, _out| {
1008 let path = args.first().map(|a| a.display()).unwrap_or_default();
1009 let metadata = std::fs::metadata(&path).map_err(|e| {
1010 VmError::Thrown(VmValue::String(Rc::from(format!(
1011 "Failed to stat {path}: {e}"
1012 ))))
1013 })?;
1014 let mut info = BTreeMap::new();
1015 info.insert("size".to_string(), VmValue::Int(metadata.len() as i64));
1016 info.insert("is_file".to_string(), VmValue::Bool(metadata.is_file()));
1017 info.insert("is_dir".to_string(), VmValue::Bool(metadata.is_dir()));
1018 info.insert(
1019 "readonly".to_string(),
1020 VmValue::Bool(metadata.permissions().readonly()),
1021 );
1022 if let Ok(modified) = metadata.modified() {
1023 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
1024 info.insert("modified".to_string(), VmValue::Float(dur.as_secs_f64()));
1025 }
1026 }
1027 Ok(VmValue::Dict(Rc::new(info)))
1028 });
1029
1030 vm.register_builtin("exec", |args, _out| {
1035 if args.is_empty() {
1036 return Err(VmError::Thrown(VmValue::String(Rc::from(
1037 "exec: command is required",
1038 ))));
1039 }
1040 let cmd = args[0].display();
1041 let cmd_args: Vec<String> = args[1..].iter().map(|a| a.display()).collect();
1042 let output = std::process::Command::new(&cmd)
1043 .args(&cmd_args)
1044 .output()
1045 .map_err(|e| VmError::Thrown(VmValue::String(Rc::from(format!("exec failed: {e}")))))?;
1046 Ok(vm_output_to_value(output))
1047 });
1048
1049 vm.register_builtin("shell", |args, _out| {
1050 let cmd = args.first().map(|a| a.display()).unwrap_or_default();
1051 if cmd.is_empty() {
1052 return Err(VmError::Thrown(VmValue::String(Rc::from(
1053 "shell: command string is required",
1054 ))));
1055 }
1056 let shell = if cfg!(target_os = "windows") {
1057 "cmd"
1058 } else {
1059 "sh"
1060 };
1061 let flag = if cfg!(target_os = "windows") {
1062 "/C"
1063 } else {
1064 "-c"
1065 };
1066 let output = std::process::Command::new(shell)
1067 .arg(flag)
1068 .arg(&cmd)
1069 .output()
1070 .map_err(|e| {
1071 VmError::Thrown(VmValue::String(Rc::from(format!("shell failed: {e}"))))
1072 })?;
1073 Ok(vm_output_to_value(output))
1074 });
1075
1076 vm.register_builtin("date_now", |_args, _out| {
1081 use std::time::{SystemTime, UNIX_EPOCH};
1082 let now = SystemTime::now()
1083 .duration_since(UNIX_EPOCH)
1084 .unwrap_or_default();
1085 let total_secs = now.as_secs();
1086 let (y, m, d, hour, minute, second, dow) = vm_civil_from_timestamp(total_secs);
1087 let mut result = BTreeMap::new();
1088 result.insert("year".to_string(), VmValue::Int(y));
1089 result.insert("month".to_string(), VmValue::Int(m));
1090 result.insert("day".to_string(), VmValue::Int(d));
1091 result.insert("hour".to_string(), VmValue::Int(hour));
1092 result.insert("minute".to_string(), VmValue::Int(minute));
1093 result.insert("second".to_string(), VmValue::Int(second));
1094 result.insert("weekday".to_string(), VmValue::Int(dow));
1095 result.insert("timestamp".to_string(), VmValue::Float(now.as_secs_f64()));
1096 Ok(VmValue::Dict(Rc::new(result)))
1097 });
1098
1099 vm.register_builtin("date_format", |args, _out| {
1100 let ts = match args.first() {
1101 Some(VmValue::Float(f)) => *f,
1102 Some(VmValue::Int(n)) => *n as f64,
1103 Some(VmValue::Dict(map)) => map
1104 .get("timestamp")
1105 .and_then(|v| match v {
1106 VmValue::Float(f) => Some(*f),
1107 VmValue::Int(n) => Some(*n as f64),
1108 _ => None,
1109 })
1110 .unwrap_or(0.0),
1111 _ => 0.0,
1112 };
1113 let fmt = args
1114 .get(1)
1115 .map(|a| a.display())
1116 .unwrap_or_else(|| "%Y-%m-%d %H:%M:%S".to_string());
1117
1118 let (y, m, d, hour, minute, second, _dow) = vm_civil_from_timestamp(ts as u64);
1119
1120 let result = fmt
1121 .replace("%Y", &format!("{y:04}"))
1122 .replace("%m", &format!("{m:02}"))
1123 .replace("%d", &format!("{d:02}"))
1124 .replace("%H", &format!("{hour:02}"))
1125 .replace("%M", &format!("{minute:02}"))
1126 .replace("%S", &format!("{second:02}"));
1127
1128 Ok(VmValue::String(Rc::from(result.as_str())))
1129 });
1130
1131 vm.register_builtin("date_parse", |args, _out| {
1132 let s = args.first().map(|a| a.display()).unwrap_or_default();
1133 let parts: Vec<&str> = s.split(|c: char| !c.is_ascii_digit()).collect();
1134 let parts: Vec<i64> = parts.iter().filter_map(|p| p.parse().ok()).collect();
1135 if parts.len() < 3 {
1136 return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1137 "Cannot parse date: {s}"
1138 )))));
1139 }
1140 let (y, m, d) = (parts[0], parts[1], parts[2]);
1141 let hour = parts.get(3).copied().unwrap_or(0);
1142 let minute = parts.get(4).copied().unwrap_or(0);
1143 let second = parts.get(5).copied().unwrap_or(0);
1144
1145 let (y_adj, m_adj) = if m <= 2 {
1146 (y - 1, (m + 9) as u64)
1147 } else {
1148 (y, (m - 3) as u64)
1149 };
1150 let era = if y_adj >= 0 { y_adj } else { y_adj - 399 } / 400;
1151 let yoe = (y_adj - era * 400) as u64;
1152 let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
1153 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
1154 let days = era * 146097 + doe as i64 - 719468;
1155 let ts = days * 86400 + hour * 3600 + minute * 60 + second;
1156 Ok(VmValue::Float(ts as f64))
1157 });
1158
1159 vm.register_builtin("format", |args, _out| {
1164 let template = args.first().map(|a| a.display()).unwrap_or_default();
1165 let mut result = String::with_capacity(template.len());
1166 let mut arg_iter = args.iter().skip(1);
1167 let mut rest = template.as_str();
1168 while let Some(pos) = rest.find("{}") {
1169 result.push_str(&rest[..pos]);
1170 if let Some(arg) = arg_iter.next() {
1171 result.push_str(&arg.display());
1172 } else {
1173 result.push_str("{}");
1174 }
1175 rest = &rest[pos + 2..];
1176 }
1177 result.push_str(rest);
1178 Ok(VmValue::String(Rc::from(result.as_str())))
1179 });
1180
1181 vm.register_builtin("trim", |args, _out| {
1186 let s = args.first().map(|a| a.display()).unwrap_or_default();
1187 Ok(VmValue::String(Rc::from(s.trim())))
1188 });
1189
1190 vm.register_builtin("lowercase", |args, _out| {
1191 let s = args.first().map(|a| a.display()).unwrap_or_default();
1192 Ok(VmValue::String(Rc::from(s.to_lowercase().as_str())))
1193 });
1194
1195 vm.register_builtin("uppercase", |args, _out| {
1196 let s = args.first().map(|a| a.display()).unwrap_or_default();
1197 Ok(VmValue::String(Rc::from(s.to_uppercase().as_str())))
1198 });
1199
1200 vm.register_builtin("split", |args, _out| {
1201 let s = args.first().map(|a| a.display()).unwrap_or_default();
1202 let sep = args
1203 .get(1)
1204 .map(|a| a.display())
1205 .unwrap_or_else(|| " ".to_string());
1206 let parts: Vec<VmValue> = s
1207 .split(&sep)
1208 .map(|p| VmValue::String(Rc::from(p)))
1209 .collect();
1210 Ok(VmValue::List(Rc::new(parts)))
1211 });
1212
1213 vm.register_builtin("starts_with", |args, _out| {
1214 let s = args.first().map(|a| a.display()).unwrap_or_default();
1215 let prefix = args.get(1).map(|a| a.display()).unwrap_or_default();
1216 Ok(VmValue::Bool(s.starts_with(&prefix)))
1217 });
1218
1219 vm.register_builtin("ends_with", |args, _out| {
1220 let s = args.first().map(|a| a.display()).unwrap_or_default();
1221 let suffix = args.get(1).map(|a| a.display()).unwrap_or_default();
1222 Ok(VmValue::Bool(s.ends_with(&suffix)))
1223 });
1224
1225 vm.register_builtin("contains", |args, _out| {
1226 match args.first().unwrap_or(&VmValue::Nil) {
1227 VmValue::String(s) => {
1228 let substr = args.get(1).map(|a| a.display()).unwrap_or_default();
1229 Ok(VmValue::Bool(s.contains(&substr)))
1230 }
1231 VmValue::List(items) => {
1232 let target = args.get(1).unwrap_or(&VmValue::Nil);
1233 Ok(VmValue::Bool(
1234 items.iter().any(|item| values_equal(item, target)),
1235 ))
1236 }
1237 _ => Ok(VmValue::Bool(false)),
1238 }
1239 });
1240
1241 vm.register_builtin("replace", |args, _out| {
1242 let s = args.first().map(|a| a.display()).unwrap_or_default();
1243 let old = args.get(1).map(|a| a.display()).unwrap_or_default();
1244 let new = args.get(2).map(|a| a.display()).unwrap_or_default();
1245 Ok(VmValue::String(Rc::from(s.replace(&old, &new).as_str())))
1246 });
1247
1248 vm.register_builtin("join", |args, _out| {
1249 let sep = args.get(1).map(|a| a.display()).unwrap_or_default();
1250 match args.first() {
1251 Some(VmValue::List(items)) => {
1252 let parts: Vec<String> = items.iter().map(|v| v.display()).collect();
1253 Ok(VmValue::String(Rc::from(parts.join(&sep).as_str())))
1254 }
1255 _ => Ok(VmValue::String(Rc::from(""))),
1256 }
1257 });
1258
1259 vm.register_builtin("len", |args, _out| {
1260 match args.first().unwrap_or(&VmValue::Nil) {
1261 VmValue::String(s) => Ok(VmValue::Int(s.len() as i64)),
1262 VmValue::List(items) => Ok(VmValue::Int(items.len() as i64)),
1263 VmValue::Dict(map) => Ok(VmValue::Int(map.len() as i64)),
1264 VmValue::Set(s) => Ok(VmValue::Int(s.len() as i64)),
1265 _ => Ok(VmValue::Int(0)),
1266 }
1267 });
1268
1269 vm.register_builtin("substring", |args, _out| {
1270 let s = args.first().map(|a| a.display()).unwrap_or_default();
1271 let start = args.get(1).and_then(|a| a.as_int()).unwrap_or(0) as usize;
1272 let start = start.min(s.len());
1273 match args.get(2).and_then(|a| a.as_int()) {
1274 Some(length) => {
1275 let length = (length as usize).min(s.len() - start);
1276 Ok(VmValue::String(Rc::from(&s[start..start + length])))
1277 }
1278 None => Ok(VmValue::String(Rc::from(&s[start..]))),
1279 }
1280 });
1281
1282 vm.register_builtin("dirname", |args, _out| {
1287 let path = args.first().map(|a| a.display()).unwrap_or_default();
1288 let p = std::path::Path::new(&path);
1289 match p.parent() {
1290 Some(parent) => Ok(VmValue::String(Rc::from(parent.to_string_lossy().as_ref()))),
1291 None => Ok(VmValue::String(Rc::from(""))),
1292 }
1293 });
1294
1295 vm.register_builtin("basename", |args, _out| {
1296 let path = args.first().map(|a| a.display()).unwrap_or_default();
1297 let p = std::path::Path::new(&path);
1298 match p.file_name() {
1299 Some(name) => Ok(VmValue::String(Rc::from(name.to_string_lossy().as_ref()))),
1300 None => Ok(VmValue::String(Rc::from(""))),
1301 }
1302 });
1303
1304 vm.register_builtin("extname", |args, _out| {
1305 let path = args.first().map(|a| a.display()).unwrap_or_default();
1306 let p = std::path::Path::new(&path);
1307 match p.extension() {
1308 Some(ext) => Ok(VmValue::String(Rc::from(
1309 format!(".{}", ext.to_string_lossy()).as_str(),
1310 ))),
1311 None => Ok(VmValue::String(Rc::from(""))),
1312 }
1313 });
1314
1315 vm.register_builtin("render", |args, _out| {
1320 let path = args.first().map(|a| a.display()).unwrap_or_default();
1321 let template = std::fs::read_to_string(&path).map_err(|e| {
1322 VmError::Thrown(VmValue::String(Rc::from(format!(
1323 "Failed to read template {path}: {e}"
1324 ))))
1325 })?;
1326 if let Some(bindings) = args.get(1).and_then(|a| a.as_dict()) {
1327 let mut result = template;
1328 for (key, val) in bindings.iter() {
1329 result = result.replace(&format!("{{{{{key}}}}}"), &val.display());
1330 }
1331 Ok(VmValue::String(Rc::from(result)))
1332 } else {
1333 Ok(VmValue::String(Rc::from(template)))
1334 }
1335 });
1336
1337 vm.register_builtin("log_debug", |args, out| {
1342 vm_write_log("debug", 0, args, out);
1343 Ok(VmValue::Nil)
1344 });
1345
1346 vm.register_builtin("log_info", |args, out| {
1347 vm_write_log("info", 1, args, out);
1348 Ok(VmValue::Nil)
1349 });
1350
1351 vm.register_builtin("log_warn", |args, out| {
1352 vm_write_log("warn", 2, args, out);
1353 Ok(VmValue::Nil)
1354 });
1355
1356 vm.register_builtin("log_error", |args, out| {
1357 vm_write_log("error", 3, args, out);
1358 Ok(VmValue::Nil)
1359 });
1360
1361 vm.register_builtin("log_set_level", |args, _out| {
1362 let level_str = args.first().map(|a| a.display()).unwrap_or_default();
1363 match vm_level_to_u8(&level_str) {
1364 Some(n) => {
1365 VM_MIN_LOG_LEVEL.store(n, Ordering::Relaxed);
1366 Ok(VmValue::Nil)
1367 }
1368 None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1369 "log_set_level: invalid level '{}'. Expected debug, info, warn, or error",
1370 level_str
1371 ))))),
1372 }
1373 });
1374
1375 vm.register_builtin("trace_start", |args, _out| {
1380 use rand::Rng;
1381 let name = args.first().map(|a| a.display()).unwrap_or_default();
1382 let trace_id = VM_TRACE_STACK.with(|stack| {
1383 stack
1384 .borrow()
1385 .last()
1386 .map(|t| t.trace_id.clone())
1387 .unwrap_or_else(|| {
1388 let val: u32 = rand::thread_rng().gen();
1389 format!("{val:08x}")
1390 })
1391 });
1392 let span_id = {
1393 let val: u32 = rand::thread_rng().gen();
1394 format!("{val:08x}")
1395 };
1396 let start_ms = std::time::SystemTime::now()
1397 .duration_since(std::time::UNIX_EPOCH)
1398 .unwrap_or_default()
1399 .as_millis() as i64;
1400
1401 VM_TRACE_STACK.with(|stack| {
1402 stack.borrow_mut().push(VmTraceContext {
1403 trace_id: trace_id.clone(),
1404 span_id: span_id.clone(),
1405 });
1406 });
1407
1408 let mut span = BTreeMap::new();
1409 span.insert(
1410 "trace_id".to_string(),
1411 VmValue::String(Rc::from(trace_id.as_str())),
1412 );
1413 span.insert(
1414 "span_id".to_string(),
1415 VmValue::String(Rc::from(span_id.as_str())),
1416 );
1417 span.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1418 span.insert("start_ms".to_string(), VmValue::Int(start_ms));
1419 Ok(VmValue::Dict(Rc::new(span)))
1420 });
1421
1422 vm.register_builtin("trace_end", |args, out| {
1423 let span = match args.first() {
1424 Some(VmValue::Dict(d)) => d,
1425 _ => {
1426 return Err(VmError::Thrown(VmValue::String(Rc::from(
1427 "trace_end: argument must be a span dict from trace_start",
1428 ))));
1429 }
1430 };
1431
1432 let end_ms = std::time::SystemTime::now()
1433 .duration_since(std::time::UNIX_EPOCH)
1434 .unwrap_or_default()
1435 .as_millis() as i64;
1436
1437 let start_ms = span
1438 .get("start_ms")
1439 .and_then(|v| v.as_int())
1440 .unwrap_or(end_ms);
1441 let duration_ms = end_ms - start_ms;
1442 let name = span.get("name").map(|v| v.display()).unwrap_or_default();
1443 let trace_id = span
1444 .get("trace_id")
1445 .map(|v| v.display())
1446 .unwrap_or_default();
1447 let span_id = span.get("span_id").map(|v| v.display()).unwrap_or_default();
1448
1449 VM_TRACE_STACK.with(|stack| {
1450 stack.borrow_mut().pop();
1451 });
1452
1453 let level_num = 1_u8;
1454 if level_num >= VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
1455 let mut fields = BTreeMap::new();
1456 fields.insert(
1457 "trace_id".to_string(),
1458 VmValue::String(Rc::from(trace_id.as_str())),
1459 );
1460 fields.insert(
1461 "span_id".to_string(),
1462 VmValue::String(Rc::from(span_id.as_str())),
1463 );
1464 fields.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1465 fields.insert("duration_ms".to_string(), VmValue::Int(duration_ms));
1466 let line = vm_build_log_line("info", "span_end", Some(&fields));
1467 out.push_str(&line);
1468 }
1469
1470 Ok(VmValue::Nil)
1471 });
1472
1473 vm.register_builtin("trace_id", |_args, _out| {
1474 let id = VM_TRACE_STACK.with(|stack| stack.borrow().last().map(|t| t.trace_id.clone()));
1475 match id {
1476 Some(trace_id) => Ok(VmValue::String(Rc::from(trace_id.as_str()))),
1477 None => Ok(VmValue::Nil),
1478 }
1479 });
1480
1481 vm.register_builtin("llm_info", |_args, _out| {
1486 let provider = std::env::var("HARN_LLM_PROVIDER").unwrap_or_default();
1487 let model = std::env::var("HARN_LLM_MODEL").unwrap_or_default();
1488 let api_key_set = std::env::var("HARN_API_KEY")
1489 .or_else(|_| std::env::var("OPENROUTER_API_KEY"))
1490 .or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
1491 .is_ok();
1492 let mut info = BTreeMap::new();
1493 info.insert(
1494 "provider".to_string(),
1495 VmValue::String(Rc::from(provider.as_str())),
1496 );
1497 info.insert(
1498 "model".to_string(),
1499 VmValue::String(Rc::from(model.as_str())),
1500 );
1501 info.insert("api_key_set".to_string(), VmValue::Bool(api_key_set));
1502 Ok(VmValue::Dict(Rc::new(info)))
1503 });
1504
1505 vm.register_builtin("llm_usage", |_args, _out| {
1506 let (total_input, total_output, total_duration, call_count) =
1507 crate::llm::peek_trace_summary();
1508 let mut usage = BTreeMap::new();
1509 usage.insert("input_tokens".to_string(), VmValue::Int(total_input));
1510 usage.insert("output_tokens".to_string(), VmValue::Int(total_output));
1511 usage.insert(
1512 "total_duration_ms".to_string(),
1513 VmValue::Int(total_duration),
1514 );
1515 usage.insert("call_count".to_string(), VmValue::Int(call_count));
1516 Ok(VmValue::Dict(Rc::new(usage)))
1517 });
1518
1519 vm.register_builtin("timer_start", |args, _out| {
1524 let name = args
1525 .first()
1526 .map(|a| a.display())
1527 .unwrap_or_else(|| "default".to_string());
1528 let now_ms = std::time::SystemTime::now()
1529 .duration_since(std::time::UNIX_EPOCH)
1530 .unwrap_or_default()
1531 .as_millis() as i64;
1532 let mut timer = BTreeMap::new();
1533 timer.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1534 timer.insert("start_ms".to_string(), VmValue::Int(now_ms));
1535 Ok(VmValue::Dict(Rc::new(timer)))
1536 });
1537
1538 vm.register_builtin("timer_end", |args, out| {
1539 let timer = match args.first() {
1540 Some(VmValue::Dict(d)) => d,
1541 _ => {
1542 return Err(VmError::Thrown(VmValue::String(Rc::from(
1543 "timer_end: argument must be a timer dict from timer_start",
1544 ))));
1545 }
1546 };
1547 let now_ms = std::time::SystemTime::now()
1548 .duration_since(std::time::UNIX_EPOCH)
1549 .unwrap_or_default()
1550 .as_millis() as i64;
1551 let start_ms = timer
1552 .get("start_ms")
1553 .and_then(|v| v.as_int())
1554 .unwrap_or(now_ms);
1555 let elapsed = now_ms - start_ms;
1556 let name = timer.get("name").map(|v| v.display()).unwrap_or_default();
1557 out.push_str(&format!("[timer] {name}: {elapsed}ms\n"));
1558 Ok(VmValue::Int(elapsed))
1559 });
1560
1561 vm.register_builtin("elapsed", |_args, _out| {
1562 static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
1565 let start = START.get_or_init(std::time::Instant::now);
1566 Ok(VmValue::Int(start.elapsed().as_millis() as i64))
1567 });
1568
1569 vm.register_builtin("log_json", |args, out| {
1570 let key = args.first().map(|a| a.display()).unwrap_or_default();
1571 let value = args.get(1).cloned().unwrap_or(VmValue::Nil);
1572 let json_val = vm_value_to_json_fragment(&value);
1573 let ts = vm_format_timestamp_utc();
1574 out.push_str(&format!(
1575 "{{\"ts\":{},\"key\":{},\"value\":{}}}\n",
1576 vm_escape_json_str_quoted(&ts),
1577 vm_escape_json_str_quoted(&key),
1578 json_val,
1579 ));
1580 Ok(VmValue::Nil)
1581 });
1582
1583 vm.register_builtin("tool_registry", |_args, _out| {
1588 let mut registry = BTreeMap::new();
1589 registry.insert(
1590 "_type".to_string(),
1591 VmValue::String(Rc::from("tool_registry")),
1592 );
1593 registry.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
1594 Ok(VmValue::Dict(Rc::new(registry)))
1595 });
1596
1597 vm.register_builtin("tool_add", |args, _out| {
1598 if args.len() < 4 {
1599 return Err(VmError::Thrown(VmValue::String(Rc::from(
1600 "tool_add: requires registry, name, description, and handler",
1601 ))));
1602 }
1603
1604 let registry = match &args[0] {
1605 VmValue::Dict(map) => (**map).clone(),
1606 _ => {
1607 return Err(VmError::Thrown(VmValue::String(Rc::from(
1608 "tool_add: first argument must be a tool registry",
1609 ))));
1610 }
1611 };
1612
1613 match registry.get("_type") {
1614 Some(VmValue::String(t)) if &**t == "tool_registry" => {}
1615 _ => {
1616 return Err(VmError::Thrown(VmValue::String(Rc::from(
1617 "tool_add: first argument must be a tool registry",
1618 ))));
1619 }
1620 }
1621
1622 let name = args[1].display();
1623 let description = args[2].display();
1624 let handler = args[3].clone();
1625 let parameters = if args.len() > 4 {
1626 args[4].clone()
1627 } else {
1628 VmValue::Dict(Rc::new(BTreeMap::new()))
1629 };
1630
1631 let mut tool_entry = BTreeMap::new();
1632 tool_entry.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1633 tool_entry.insert(
1634 "description".to_string(),
1635 VmValue::String(Rc::from(description.as_str())),
1636 );
1637 tool_entry.insert("handler".to_string(), handler);
1638 tool_entry.insert("parameters".to_string(), parameters);
1639
1640 let mut tools: Vec<VmValue> = match registry.get("tools") {
1641 Some(VmValue::List(list)) => list
1642 .iter()
1643 .filter(|t| {
1644 if let VmValue::Dict(e) = t {
1645 e.get("name").map(|v| v.display()).as_deref() != Some(name.as_str())
1646 } else {
1647 true
1648 }
1649 })
1650 .cloned()
1651 .collect(),
1652 _ => Vec::new(),
1653 };
1654 tools.push(VmValue::Dict(Rc::new(tool_entry)));
1655
1656 let mut new_registry = registry;
1657 new_registry.insert("tools".to_string(), VmValue::List(Rc::new(tools)));
1658 Ok(VmValue::Dict(Rc::new(new_registry)))
1659 });
1660
1661 vm.register_builtin("tool_list", |args, _out| {
1662 let registry = match args.first() {
1663 Some(VmValue::Dict(map)) => map,
1664 _ => {
1665 return Err(VmError::Thrown(VmValue::String(Rc::from(
1666 "tool_list: requires a tool registry",
1667 ))));
1668 }
1669 };
1670 vm_validate_registry("tool_list", registry)?;
1671
1672 let tools = vm_get_tools(registry);
1673 let mut result = Vec::new();
1674 for tool in tools {
1675 if let VmValue::Dict(entry) = tool {
1676 let mut desc = BTreeMap::new();
1677 if let Some(name) = entry.get("name") {
1678 desc.insert("name".to_string(), name.clone());
1679 }
1680 if let Some(description) = entry.get("description") {
1681 desc.insert("description".to_string(), description.clone());
1682 }
1683 if let Some(parameters) = entry.get("parameters") {
1684 desc.insert("parameters".to_string(), parameters.clone());
1685 }
1686 result.push(VmValue::Dict(Rc::new(desc)));
1687 }
1688 }
1689 Ok(VmValue::List(Rc::new(result)))
1690 });
1691
1692 vm.register_builtin("tool_find", |args, _out| {
1693 if args.len() < 2 {
1694 return Err(VmError::Thrown(VmValue::String(Rc::from(
1695 "tool_find: requires registry and name",
1696 ))));
1697 }
1698
1699 let registry = match &args[0] {
1700 VmValue::Dict(map) => map,
1701 _ => {
1702 return Err(VmError::Thrown(VmValue::String(Rc::from(
1703 "tool_find: first argument must be a tool registry",
1704 ))));
1705 }
1706 };
1707 vm_validate_registry("tool_find", registry)?;
1708
1709 let target_name = args[1].display();
1710 let tools = vm_get_tools(registry);
1711
1712 for tool in tools {
1713 if let VmValue::Dict(entry) = tool {
1714 if let Some(VmValue::String(name)) = entry.get("name") {
1715 if &**name == target_name.as_str() {
1716 return Ok(tool.clone());
1717 }
1718 }
1719 }
1720 }
1721 Ok(VmValue::Nil)
1722 });
1723
1724 vm.register_builtin("tool_describe", |args, _out| {
1725 let registry = match args.first() {
1726 Some(VmValue::Dict(map)) => map,
1727 _ => {
1728 return Err(VmError::Thrown(VmValue::String(Rc::from(
1729 "tool_describe: requires a tool registry",
1730 ))));
1731 }
1732 };
1733 vm_validate_registry("tool_describe", registry)?;
1734
1735 let tools = vm_get_tools(registry);
1736
1737 if tools.is_empty() {
1738 return Ok(VmValue::String(Rc::from("Available tools:\n(none)")));
1739 }
1740
1741 let mut tool_infos: Vec<(String, String, String)> = Vec::new();
1742 for tool in tools {
1743 if let VmValue::Dict(entry) = tool {
1744 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1745 let description = entry
1746 .get("description")
1747 .map(|v| v.display())
1748 .unwrap_or_default();
1749 let params_str = vm_format_parameters(entry.get("parameters"));
1750 tool_infos.push((name, params_str, description));
1751 }
1752 }
1753
1754 tool_infos.sort_by(|a, b| a.0.cmp(&b.0));
1755
1756 let mut lines = vec!["Available tools:".to_string()];
1757 for (name, params, desc) in &tool_infos {
1758 lines.push(format!("- {name}({params}): {desc}"));
1759 }
1760
1761 Ok(VmValue::String(Rc::from(lines.join("\n").as_str())))
1762 });
1763
1764 vm.register_builtin("tool_remove", |args, _out| {
1765 if args.len() < 2 {
1766 return Err(VmError::Thrown(VmValue::String(Rc::from(
1767 "tool_remove: requires registry and name",
1768 ))));
1769 }
1770
1771 let registry = match &args[0] {
1772 VmValue::Dict(map) => (**map).clone(),
1773 _ => {
1774 return Err(VmError::Thrown(VmValue::String(Rc::from(
1775 "tool_remove: first argument must be a tool registry",
1776 ))));
1777 }
1778 };
1779 vm_validate_registry("tool_remove", ®istry)?;
1780
1781 let target_name = args[1].display();
1782
1783 let tools = match registry.get("tools") {
1784 Some(VmValue::List(list)) => (**list).clone(),
1785 _ => Vec::new(),
1786 };
1787
1788 let filtered: Vec<VmValue> = tools
1789 .into_iter()
1790 .filter(|tool| {
1791 if let VmValue::Dict(entry) = tool {
1792 if let Some(VmValue::String(name)) = entry.get("name") {
1793 return &**name != target_name.as_str();
1794 }
1795 }
1796 true
1797 })
1798 .collect();
1799
1800 let mut new_registry = registry;
1801 new_registry.insert("tools".to_string(), VmValue::List(Rc::new(filtered)));
1802 Ok(VmValue::Dict(Rc::new(new_registry)))
1803 });
1804
1805 vm.register_builtin("tool_count", |args, _out| {
1806 let registry = match args.first() {
1807 Some(VmValue::Dict(map)) => map,
1808 _ => {
1809 return Err(VmError::Thrown(VmValue::String(Rc::from(
1810 "tool_count: requires a tool registry",
1811 ))));
1812 }
1813 };
1814 vm_validate_registry("tool_count", registry)?;
1815 let count = vm_get_tools(registry).len();
1816 Ok(VmValue::Int(count as i64))
1817 });
1818
1819 vm.register_builtin("tool_schema", |args, _out| {
1820 let registry = match args.first() {
1821 Some(VmValue::Dict(map)) => {
1822 vm_validate_registry("tool_schema", map)?;
1823 map
1824 }
1825 _ => {
1826 return Err(VmError::Thrown(VmValue::String(Rc::from(
1827 "tool_schema: requires a tool registry",
1828 ))));
1829 }
1830 };
1831
1832 let components = args.get(1).and_then(|v| v.as_dict()).cloned();
1833
1834 let tools = match registry.get("tools") {
1835 Some(VmValue::List(list)) => list,
1836 _ => return Ok(VmValue::Dict(Rc::new(vm_build_empty_schema()))),
1837 };
1838
1839 let mut tool_schemas = Vec::new();
1840 for tool in tools.iter() {
1841 if let VmValue::Dict(entry) = tool {
1842 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1843 let description = entry
1844 .get("description")
1845 .map(|v| v.display())
1846 .unwrap_or_default();
1847
1848 let input_schema =
1849 vm_build_input_schema(entry.get("parameters"), components.as_ref());
1850
1851 let mut tool_def = BTreeMap::new();
1852 tool_def.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1853 tool_def.insert(
1854 "description".to_string(),
1855 VmValue::String(Rc::from(description.as_str())),
1856 );
1857 tool_def.insert("inputSchema".to_string(), input_schema);
1858 tool_schemas.push(VmValue::Dict(Rc::new(tool_def)));
1859 }
1860 }
1861
1862 let mut schema = BTreeMap::new();
1863 schema.insert(
1864 "schema_version".to_string(),
1865 VmValue::String(Rc::from("harn-tools/1.0")),
1866 );
1867
1868 if let Some(comps) = &components {
1869 let mut comp_wrapper = BTreeMap::new();
1870 comp_wrapper.insert("schemas".to_string(), VmValue::Dict(Rc::new(comps.clone())));
1871 schema.insert(
1872 "components".to_string(),
1873 VmValue::Dict(Rc::new(comp_wrapper)),
1874 );
1875 }
1876
1877 schema.insert("tools".to_string(), VmValue::List(Rc::new(tool_schemas)));
1878 Ok(VmValue::Dict(Rc::new(schema)))
1879 });
1880
1881 vm.register_builtin("tool_parse_call", |args, _out| {
1882 let text = args.first().map(|a| a.display()).unwrap_or_default();
1883
1884 let mut results = Vec::new();
1885 let mut search_from = 0;
1886
1887 while let Some(start) = text[search_from..].find("<tool_call>") {
1888 let abs_start = search_from + start + "<tool_call>".len();
1889 if let Some(end) = text[abs_start..].find("</tool_call>") {
1890 let json_str = text[abs_start..abs_start + end].trim();
1891 if let Ok(jv) = serde_json::from_str::<serde_json::Value>(json_str) {
1892 results.push(json_to_vm_value(&jv));
1893 }
1894 search_from = abs_start + end + "</tool_call>".len();
1895 } else {
1896 break;
1897 }
1898 }
1899
1900 Ok(VmValue::List(Rc::new(results)))
1901 });
1902
1903 vm.register_builtin("tool_format_result", |args, _out| {
1904 if args.len() < 2 {
1905 return Err(VmError::Thrown(VmValue::String(Rc::from(
1906 "tool_format_result: requires name and result",
1907 ))));
1908 }
1909 let name = args[0].display();
1910 let result = args[1].display();
1911
1912 let json_name = vm_escape_json_str(&name);
1913 let json_result = vm_escape_json_str(&result);
1914 Ok(VmValue::String(Rc::from(
1915 format!(
1916 "<tool_result>{{\"name\": \"{json_name}\", \"result\": \"{json_result}\"}}</tool_result>"
1917 )
1918 .as_str(),
1919 )))
1920 });
1921
1922 vm.register_builtin("tool_prompt", |args, _out| {
1923 let registry = match args.first() {
1924 Some(VmValue::Dict(map)) => {
1925 vm_validate_registry("tool_prompt", map)?;
1926 map
1927 }
1928 _ => {
1929 return Err(VmError::Thrown(VmValue::String(Rc::from(
1930 "tool_prompt: requires a tool registry",
1931 ))));
1932 }
1933 };
1934
1935 let tools = match registry.get("tools") {
1936 Some(VmValue::List(list)) => list,
1937 _ => {
1938 return Ok(VmValue::String(Rc::from("No tools are available.")));
1939 }
1940 };
1941
1942 if tools.is_empty() {
1943 return Ok(VmValue::String(Rc::from("No tools are available.")));
1944 }
1945
1946 let mut prompt = String::from("# Available Tools\n\n");
1947 prompt.push_str("You have access to the following tools. To use a tool, output a tool call in this exact format:\n\n");
1948 prompt.push_str("<tool_call>{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}</tool_call>\n\n");
1949 prompt.push_str("You may make multiple tool calls in a single response. Wait for tool results before proceeding.\n\n");
1950 prompt.push_str("## Tools\n\n");
1951
1952 let mut tool_infos: Vec<(&BTreeMap<String, VmValue>, String)> = Vec::new();
1953 for tool in tools.iter() {
1954 if let VmValue::Dict(entry) = tool {
1955 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1956 tool_infos.push((entry, name));
1957 }
1958 }
1959 tool_infos.sort_by(|a, b| a.1.cmp(&b.1));
1960
1961 for (entry, name) in &tool_infos {
1962 let description = entry
1963 .get("description")
1964 .map(|v| v.display())
1965 .unwrap_or_default();
1966 let params_str = vm_format_parameters(entry.get("parameters"));
1967
1968 prompt.push_str(&format!("### {name}\n"));
1969 prompt.push_str(&format!("{description}\n"));
1970 if !params_str.is_empty() {
1971 prompt.push_str(&format!("Parameters: {params_str}\n"));
1972 }
1973 prompt.push('\n');
1974 }
1975
1976 Ok(VmValue::String(Rc::from(prompt.trim_end())))
1977 });
1978
1979 vm.register_builtin("channel", |args, _out| {
1984 let name = args
1985 .first()
1986 .map(|a| a.display())
1987 .unwrap_or_else(|| "default".to_string());
1988 let capacity = args.get(1).and_then(|a| a.as_int()).unwrap_or(256) as usize;
1989 let capacity = capacity.max(1);
1990 let (tx, rx) = tokio::sync::mpsc::channel(capacity);
1991 #[allow(clippy::arc_with_non_send_sync)]
1992 Ok(VmValue::Channel(VmChannelHandle {
1993 name,
1994 sender: Arc::new(tx),
1995 receiver: Arc::new(tokio::sync::Mutex::new(rx)),
1996 closed: Arc::new(AtomicBool::new(false)),
1997 }))
1998 });
1999
2000 vm.register_builtin("close_channel", |args, _out| {
2001 if args.is_empty() {
2002 return Err(VmError::Thrown(VmValue::String(Rc::from(
2003 "close_channel: requires a channel",
2004 ))));
2005 }
2006 if let VmValue::Channel(ch) = &args[0] {
2007 ch.closed.store(true, Ordering::SeqCst);
2008 Ok(VmValue::Nil)
2009 } else {
2010 Err(VmError::Thrown(VmValue::String(Rc::from(
2011 "close_channel: first argument must be a channel",
2012 ))))
2013 }
2014 });
2015
2016 vm.register_builtin("try_receive", |args, _out| {
2017 if args.is_empty() {
2018 return Err(VmError::Thrown(VmValue::String(Rc::from(
2019 "try_receive: requires a channel",
2020 ))));
2021 }
2022 if let VmValue::Channel(ch) = &args[0] {
2023 match ch.receiver.try_lock() {
2024 Ok(mut rx) => match rx.try_recv() {
2025 Ok(val) => Ok(val),
2026 Err(_) => Ok(VmValue::Nil),
2027 },
2028 Err(_) => Ok(VmValue::Nil),
2029 }
2030 } else {
2031 Err(VmError::Thrown(VmValue::String(Rc::from(
2032 "try_receive: first argument must be a channel",
2033 ))))
2034 }
2035 });
2036
2037 vm.register_builtin("atomic", |args, _out| {
2042 let initial = match args.first() {
2043 Some(VmValue::Int(n)) => *n,
2044 Some(VmValue::Float(f)) => *f as i64,
2045 Some(VmValue::Bool(b)) => {
2046 if *b {
2047 1
2048 } else {
2049 0
2050 }
2051 }
2052 _ => 0,
2053 };
2054 Ok(VmValue::Atomic(VmAtomicHandle {
2055 value: Arc::new(AtomicI64::new(initial)),
2056 }))
2057 });
2058
2059 vm.register_builtin("atomic_get", |args, _out| {
2060 if let Some(VmValue::Atomic(a)) = args.first() {
2061 Ok(VmValue::Int(a.value.load(Ordering::SeqCst)))
2062 } else {
2063 Ok(VmValue::Nil)
2064 }
2065 });
2066
2067 vm.register_builtin("atomic_set", |args, _out| {
2068 if args.len() >= 2 {
2069 if let (VmValue::Atomic(a), Some(val)) = (&args[0], args[1].as_int()) {
2070 let old = a.value.swap(val, Ordering::SeqCst);
2071 return Ok(VmValue::Int(old));
2072 }
2073 }
2074 Ok(VmValue::Nil)
2075 });
2076
2077 vm.register_builtin("atomic_add", |args, _out| {
2078 if args.len() >= 2 {
2079 if let (VmValue::Atomic(a), Some(delta)) = (&args[0], args[1].as_int()) {
2080 let prev = a.value.fetch_add(delta, Ordering::SeqCst);
2081 return Ok(VmValue::Int(prev));
2082 }
2083 }
2084 Ok(VmValue::Nil)
2085 });
2086
2087 vm.register_builtin("atomic_cas", |args, _out| {
2088 if args.len() >= 3 {
2089 if let (VmValue::Atomic(a), Some(expected), Some(new_val)) =
2090 (&args[0], args[1].as_int(), args[2].as_int())
2091 {
2092 let result =
2093 a.value
2094 .compare_exchange(expected, new_val, Ordering::SeqCst, Ordering::SeqCst);
2095 return Ok(VmValue::Bool(result.is_ok()));
2096 }
2097 }
2098 Ok(VmValue::Bool(false))
2099 });
2100
2101 vm.register_async_builtin("sleep", |args| async move {
2107 let ms = match args.first() {
2108 Some(VmValue::Duration(ms)) => *ms,
2109 Some(VmValue::Int(n)) => *n as u64,
2110 _ => 0,
2111 };
2112 if ms > 0 {
2113 tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
2114 }
2115 Ok(VmValue::Nil)
2116 });
2117
2118 vm.register_async_builtin("send", |args| async move {
2120 if args.len() < 2 {
2121 return Err(VmError::Thrown(VmValue::String(Rc::from(
2122 "send: requires channel and value",
2123 ))));
2124 }
2125 if let VmValue::Channel(ch) = &args[0] {
2126 if ch.closed.load(Ordering::SeqCst) {
2127 return Ok(VmValue::Bool(false));
2128 }
2129 let val = args[1].clone();
2130 match ch.sender.send(val).await {
2131 Ok(()) => Ok(VmValue::Bool(true)),
2132 Err(_) => Ok(VmValue::Bool(false)),
2133 }
2134 } else {
2135 Err(VmError::Thrown(VmValue::String(Rc::from(
2136 "send: first argument must be a channel",
2137 ))))
2138 }
2139 });
2140
2141 vm.register_async_builtin("receive", |args| async move {
2143 if args.is_empty() {
2144 return Err(VmError::Thrown(VmValue::String(Rc::from(
2145 "receive: requires a channel",
2146 ))));
2147 }
2148 if let VmValue::Channel(ch) = &args[0] {
2149 if ch.closed.load(Ordering::SeqCst) {
2150 let mut rx = ch.receiver.lock().await;
2151 return match rx.try_recv() {
2152 Ok(val) => Ok(val),
2153 Err(_) => Ok(VmValue::Nil),
2154 };
2155 }
2156 let mut rx = ch.receiver.lock().await;
2157 match rx.recv().await {
2158 Some(val) => Ok(val),
2159 None => Ok(VmValue::Nil),
2160 }
2161 } else {
2162 Err(VmError::Thrown(VmValue::String(Rc::from(
2163 "receive: first argument must be a channel",
2164 ))))
2165 }
2166 });
2167
2168 vm.register_async_builtin("select", |args| async move {
2170 if args.is_empty() {
2171 return Err(VmError::Thrown(VmValue::String(Rc::from(
2172 "select: requires at least one channel",
2173 ))));
2174 }
2175 for arg in &args {
2176 if !matches!(arg, VmValue::Channel(_)) {
2177 return Err(VmError::Thrown(VmValue::String(Rc::from(
2178 "select: all arguments must be channels",
2179 ))));
2180 }
2181 }
2182 loop {
2183 let (found, all_closed) = try_poll_channels(&args);
2184 if let Some((i, val, name)) = found {
2185 return Ok(select_result(i, val, &name));
2186 }
2187 if all_closed {
2188 return Ok(select_none());
2189 }
2190 tokio::task::yield_now().await;
2191 }
2192 });
2193
2194 vm.register_async_builtin("__select_timeout", |args| async move {
2196 if args.len() < 2 {
2197 return Err(VmError::Thrown(VmValue::String(Rc::from(
2198 "__select_timeout: requires channel list and timeout",
2199 ))));
2200 }
2201 let channels = match &args[0] {
2202 VmValue::List(items) => (**items).clone(),
2203 _ => {
2204 return Err(VmError::Thrown(VmValue::String(Rc::from(
2205 "__select_timeout: first argument must be a list of channels",
2206 ))));
2207 }
2208 };
2209 let timeout_ms = match &args[1] {
2210 VmValue::Int(n) => (*n).max(0) as u64,
2211 VmValue::Duration(ms) => *ms,
2212 _ => 5000,
2213 };
2214 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
2215 loop {
2216 let (found, all_closed) = try_poll_channels(&channels);
2217 if let Some((i, val, name)) = found {
2218 return Ok(select_result(i, val, &name));
2219 }
2220 if all_closed || tokio::time::Instant::now() >= deadline {
2221 return Ok(select_none());
2222 }
2223 tokio::task::yield_now().await;
2224 }
2225 });
2226
2227 vm.register_async_builtin("__select_try", |args| async move {
2229 if args.is_empty() {
2230 return Err(VmError::Thrown(VmValue::String(Rc::from(
2231 "__select_try: requires channel list",
2232 ))));
2233 }
2234 let channels = match &args[0] {
2235 VmValue::List(items) => (**items).clone(),
2236 _ => {
2237 return Err(VmError::Thrown(VmValue::String(Rc::from(
2238 "__select_try: first argument must be a list of channels",
2239 ))));
2240 }
2241 };
2242 let (found, _) = try_poll_channels(&channels);
2243 if let Some((i, val, name)) = found {
2244 Ok(select_result(i, val, &name))
2245 } else {
2246 Ok(select_none())
2247 }
2248 });
2249
2250 vm.register_async_builtin("__select_list", |args| async move {
2252 if args.is_empty() {
2253 return Err(VmError::Thrown(VmValue::String(Rc::from(
2254 "__select_list: requires channel list",
2255 ))));
2256 }
2257 let channels = match &args[0] {
2258 VmValue::List(items) => (**items).clone(),
2259 _ => {
2260 return Err(VmError::Thrown(VmValue::String(Rc::from(
2261 "__select_list: first argument must be a list of channels",
2262 ))));
2263 }
2264 };
2265 loop {
2266 let (found, all_closed) = try_poll_channels(&channels);
2267 if let Some((i, val, name)) = found {
2268 return Ok(select_result(i, val, &name));
2269 }
2270 if all_closed {
2271 return Ok(select_none());
2272 }
2273 tokio::task::yield_now().await;
2274 }
2275 });
2276
2277 vm.register_builtin("json_validate", |args, _out| {
2282 if args.len() < 2 {
2283 return Err(VmError::Thrown(VmValue::String(Rc::from(
2284 "json_validate requires 2 arguments: data and schema",
2285 ))));
2286 }
2287 let data = &args[0];
2288 let schema = &args[1];
2289 let schema_dict = match schema.as_dict() {
2290 Some(d) => d,
2291 None => {
2292 return Err(VmError::Thrown(VmValue::String(Rc::from(
2293 "json_validate: schema must be a dict",
2294 ))));
2295 }
2296 };
2297 let mut errors = Vec::new();
2298 validate_value(data, schema_dict, "", &mut errors);
2299 if errors.is_empty() {
2300 Ok(VmValue::Bool(true))
2301 } else {
2302 Err(VmError::Thrown(VmValue::String(Rc::from(
2303 errors.join("; "),
2304 ))))
2305 }
2306 });
2307
2308 vm.register_builtin("json_extract", |args, _out| {
2309 if args.is_empty() {
2310 return Err(VmError::Thrown(VmValue::String(Rc::from(
2311 "json_extract requires at least 1 argument: text",
2312 ))));
2313 }
2314 let text = args[0].display();
2315 let key = args.get(1).map(|a| a.display());
2316
2317 let json_str = extract_json_from_text(&text);
2319 let parsed = match serde_json::from_str::<serde_json::Value>(&json_str) {
2320 Ok(jv) => json_to_vm_value(&jv),
2321 Err(e) => {
2322 return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2323 "json_extract: failed to parse JSON: {e}"
2324 )))));
2325 }
2326 };
2327
2328 match key {
2329 Some(k) => match &parsed {
2330 VmValue::Dict(map) => match map.get(&k) {
2331 Some(val) => Ok(val.clone()),
2332 None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2333 "json_extract: key '{}' not found",
2334 k
2335 ))))),
2336 },
2337 _ => Err(VmError::Thrown(VmValue::String(Rc::from(
2338 "json_extract: parsed value is not a dict, cannot extract key",
2339 )))),
2340 },
2341 None => Ok(parsed),
2342 }
2343 });
2344
2345 vm.register_builtin("__assert_dict", |args, _out| {
2354 let val = args.first().cloned().unwrap_or(VmValue::Nil);
2355 if matches!(val, VmValue::Dict(_)) {
2356 Ok(VmValue::Nil)
2357 } else {
2358 Err(VmError::TypeError(format!(
2359 "cannot destructure {} with {{...}} pattern — expected dict",
2360 val.type_name()
2361 )))
2362 }
2363 });
2364
2365 vm.register_builtin("__assert_list", |args, _out| {
2366 let val = args.first().cloned().unwrap_or(VmValue::Nil);
2367 if matches!(val, VmValue::List(_)) {
2368 Ok(VmValue::Nil)
2369 } else {
2370 Err(VmError::TypeError(format!(
2371 "cannot destructure {} with [...] pattern — expected list",
2372 val.type_name()
2373 )))
2374 }
2375 });
2376
2377 vm.register_builtin("__assert_shape", |args, _out| {
2378 let val = args.first().cloned().unwrap_or(VmValue::Nil);
2379 let param_name = match args.get(1) {
2380 Some(VmValue::String(s)) => s.to_string(),
2381 _ => "value".to_string(),
2382 };
2383 let spec = match args.get(2) {
2384 Some(VmValue::String(s)) => s.to_string(),
2385 _ => return Ok(VmValue::Nil),
2386 };
2387
2388 let fields: Option<&BTreeMap<String, VmValue>> = match &val {
2390 VmValue::Dict(map) => Some(map.as_ref()),
2391 VmValue::StructInstance { fields, .. } => Some(fields),
2392 _ => None,
2393 };
2394 let fields = match fields {
2395 Some(f) => f,
2396 None => {
2397 return Err(VmError::TypeError(format!(
2398 "parameter '{}': expected dict or struct, got {}",
2399 param_name,
2400 val.type_name()
2401 )));
2402 }
2403 };
2404
2405 assert_shape_fields(fields, ¶m_name, &spec)
2406 });
2407
2408 vm.register_builtin("__dict_rest", |args, _out| {
2409 let dict = args.first().cloned().unwrap_or(VmValue::Nil);
2411 let keys_list = args.get(1).cloned().unwrap_or(VmValue::Nil);
2412 if let VmValue::Dict(map) = dict {
2413 let exclude: std::collections::HashSet<String> = match keys_list {
2414 VmValue::List(items) => items
2415 .iter()
2416 .filter_map(|v| {
2417 if let VmValue::String(s) = v {
2418 Some(s.to_string())
2419 } else {
2420 None
2421 }
2422 })
2423 .collect(),
2424 _ => std::collections::HashSet::new(),
2425 };
2426 let rest: BTreeMap<String, VmValue> = map
2427 .iter()
2428 .filter(|(k, _)| !exclude.contains(k.as_str()))
2429 .map(|(k, v)| (k.clone(), v.clone()))
2430 .collect();
2431 Ok(VmValue::Dict(Rc::new(rest)))
2432 } else {
2433 Ok(VmValue::Nil)
2434 }
2435 });
2436
2437 register_http_builtins(vm);
2438 register_llm_builtins(vm);
2439 register_mcp_builtins(vm);
2440}
2441
2442fn assert_shape_fields(
2449 fields: &BTreeMap<String, VmValue>,
2450 param_name: &str,
2451 spec: &str,
2452) -> Result<VmValue, VmError> {
2453 let parsed = parse_shape_spec(spec);
2454 for (field_name, type_spec, optional) in &parsed {
2455 match fields.get(field_name.as_str()) {
2456 None => {
2457 if !optional {
2458 return Err(VmError::TypeError(format!(
2459 "parameter '{}': missing field '{}' ({})",
2460 param_name, field_name, type_spec
2461 )));
2462 }
2463 }
2464 Some(val) => {
2465 if type_spec.starts_with('{') && type_spec.ends_with('}') {
2467 let inner_spec = &type_spec[1..type_spec.len() - 1];
2469 let nested_fields: Option<&BTreeMap<String, VmValue>> = match val {
2470 VmValue::Dict(map) => Some(map.as_ref()),
2471 VmValue::StructInstance { fields, .. } => Some(fields),
2472 _ => None,
2473 };
2474 match nested_fields {
2475 Some(nf) => {
2476 let nested_param = format!("{}.{}", param_name, field_name);
2477 assert_shape_fields(nf, &nested_param, inner_spec)?;
2478 }
2479 None => {
2480 return Err(VmError::TypeError(format!(
2481 "parameter '{}': field '{}' expected dict or struct, got {}",
2482 param_name,
2483 field_name,
2484 val.type_name()
2485 )));
2486 }
2487 }
2488 } else {
2489 let actual_type = val.type_name();
2491 if actual_type != type_spec.as_str() {
2492 return Err(VmError::TypeError(format!(
2493 "parameter '{}': field '{}' expected {}, got {}",
2494 param_name, field_name, type_spec, actual_type
2495 )));
2496 }
2497 }
2498 }
2499 }
2500 }
2501 Ok(VmValue::Nil)
2502}
2503
2504fn parse_shape_spec(spec: &str) -> Vec<(String, String, bool)> {
2507 let mut result = Vec::new();
2508 let chars: Vec<char> = spec.chars().collect();
2509 let len = chars.len();
2510 let mut i = 0;
2511
2512 while i < len {
2513 while i < len && chars[i].is_whitespace() {
2515 i += 1;
2516 }
2517 if i >= len {
2518 break;
2519 }
2520
2521 let name_start = i;
2523 while i < len && chars[i] != ':' {
2524 i += 1;
2525 }
2526 if i >= len {
2527 break;
2528 }
2529 let field_name = chars[name_start..i]
2530 .iter()
2531 .collect::<String>()
2532 .trim()
2533 .to_string();
2534 i += 1; while i < len && chars[i].is_whitespace() {
2538 i += 1;
2539 }
2540
2541 let optional = if i < len && chars[i] == '?' {
2543 i += 1;
2544 true
2545 } else {
2546 false
2547 };
2548
2549 let type_start = i;
2551 let mut brace_depth = 0;
2552 while i < len {
2553 match chars[i] {
2554 '{' => {
2555 brace_depth += 1;
2556 i += 1;
2557 }
2558 '}' => {
2559 brace_depth -= 1;
2560 i += 1;
2561 }
2562 ',' if brace_depth == 0 => break,
2563 _ => {
2564 i += 1;
2565 }
2566 }
2567 }
2568 let type_spec = chars[type_start..i]
2569 .iter()
2570 .collect::<String>()
2571 .trim()
2572 .to_string();
2573
2574 if !field_name.is_empty() && !type_spec.is_empty() {
2575 result.push((field_name, type_spec, optional));
2576 }
2577
2578 if i < len && chars[i] == ',' {
2580 i += 1;
2581 }
2582 }
2583
2584 result
2585}
2586
2587pub(crate) fn escape_json_string_vm(s: &str) -> String {
2592 let mut out = String::with_capacity(s.len() + 2);
2593 out.push('"');
2594 for ch in s.chars() {
2595 match ch {
2596 '"' => out.push_str("\\\""),
2597 '\\' => out.push_str("\\\\"),
2598 '\n' => out.push_str("\\n"),
2599 '\r' => out.push_str("\\r"),
2600 '\t' => out.push_str("\\t"),
2601 c if c.is_control() => {
2602 out.push_str(&format!("\\u{:04x}", c as u32));
2603 }
2604 c => out.push(c),
2605 }
2606 }
2607 out.push('"');
2608 out
2609}
2610
2611pub(crate) fn vm_value_to_json(val: &VmValue) -> String {
2612 match val {
2613 VmValue::String(s) => escape_json_string_vm(s),
2614 VmValue::Int(n) => n.to_string(),
2615 VmValue::Float(n) => n.to_string(),
2616 VmValue::Bool(b) => b.to_string(),
2617 VmValue::Nil => "null".to_string(),
2618 VmValue::List(items) => {
2619 let inner: Vec<String> = items.iter().map(vm_value_to_json).collect();
2620 format!("[{}]", inner.join(","))
2621 }
2622 VmValue::Dict(map) => {
2623 let inner: Vec<String> = map
2624 .iter()
2625 .map(|(k, v)| format!("{}:{}", escape_json_string_vm(k), vm_value_to_json(v)))
2626 .collect();
2627 format!("{{{}}}", inner.join(","))
2628 }
2629 VmValue::Set(items) => {
2630 let inner: Vec<String> = items.iter().map(vm_value_to_json).collect();
2631 format!("[{}]", inner.join(","))
2632 }
2633 _ => "null".to_string(),
2634 }
2635}
2636
2637pub(crate) fn json_to_vm_value(jv: &serde_json::Value) -> VmValue {
2638 match jv {
2639 serde_json::Value::Null => VmValue::Nil,
2640 serde_json::Value::Bool(b) => VmValue::Bool(*b),
2641 serde_json::Value::Number(n) => {
2642 if let Some(i) = n.as_i64() {
2643 VmValue::Int(i)
2644 } else {
2645 VmValue::Float(n.as_f64().unwrap_or(0.0))
2646 }
2647 }
2648 serde_json::Value::String(s) => VmValue::String(Rc::from(s.as_str())),
2649 serde_json::Value::Array(arr) => {
2650 VmValue::List(Rc::new(arr.iter().map(json_to_vm_value).collect()))
2651 }
2652 serde_json::Value::Object(map) => {
2653 let mut m = BTreeMap::new();
2654 for (k, v) in map {
2655 m.insert(k.clone(), json_to_vm_value(v));
2656 }
2657 VmValue::Dict(Rc::new(m))
2658 }
2659 }
2660}
2661
2662fn validate_value(
2667 value: &VmValue,
2668 schema: &BTreeMap<String, VmValue>,
2669 path: &str,
2670 errors: &mut Vec<String>,
2671) {
2672 if let Some(VmValue::String(expected_type)) = schema.get("type") {
2674 let actual_type = value.type_name();
2675 let type_str: &str = expected_type;
2676 if type_str != "any" && actual_type != type_str {
2677 let location = if path.is_empty() {
2678 "root".to_string()
2679 } else {
2680 path.to_string()
2681 };
2682 errors.push(format!(
2683 "at {}: expected type '{}', got '{}'",
2684 location, type_str, actual_type
2685 ));
2686 return; }
2688 }
2689
2690 if let Some(VmValue::List(required_keys)) = schema.get("required") {
2692 if let VmValue::Dict(map) = value {
2693 for key_val in required_keys.iter() {
2694 let key = key_val.display();
2695 if !map.contains_key(&key) {
2696 let location = if path.is_empty() {
2697 "root".to_string()
2698 } else {
2699 path.to_string()
2700 };
2701 errors.push(format!("at {}: missing required key '{}'", location, key));
2702 }
2703 }
2704 }
2705 }
2706
2707 if let Some(VmValue::Dict(prop_schemas)) = schema.get("properties") {
2709 if let VmValue::Dict(map) = value {
2710 for (key, prop_schema) in prop_schemas.iter() {
2711 if let Some(prop_value) = map.get(key) {
2712 if let Some(prop_schema_dict) = prop_schema.as_dict() {
2713 let child_path = if path.is_empty() {
2714 key.clone()
2715 } else {
2716 format!("{}.{}", path, key)
2717 };
2718 validate_value(prop_value, prop_schema_dict, &child_path, errors);
2719 }
2720 }
2721 }
2722 }
2723 }
2724
2725 if let Some(VmValue::Dict(item_schema)) = schema.get("items") {
2727 if let VmValue::List(items) = value {
2728 for (i, item) in items.iter().enumerate() {
2729 let child_path = if path.is_empty() {
2730 format!("[{}]", i)
2731 } else {
2732 format!("{}[{}]", path, i)
2733 };
2734 validate_value(item, item_schema, &child_path, errors);
2735 }
2736 }
2737 }
2738}
2739
2740fn extract_json_from_text(text: &str) -> String {
2745 let trimmed = text.trim();
2746
2747 if let Some(start) = trimmed.find("```") {
2749 let after_backticks = &trimmed[start + 3..];
2750 let content_start = if let Some(nl) = after_backticks.find('\n') {
2752 nl + 1
2753 } else {
2754 0
2755 };
2756 let content = &after_backticks[content_start..];
2757 if let Some(end) = content.find("```") {
2758 return content[..end].trim().to_string();
2759 }
2760 }
2761
2762 if let Some(obj_start) = trimmed.find('{') {
2765 if let Some(obj_end) = trimmed.rfind('}') {
2766 if obj_end > obj_start {
2767 return trimmed[obj_start..=obj_end].to_string();
2768 }
2769 }
2770 }
2771 if let Some(arr_start) = trimmed.find('[') {
2772 if let Some(arr_end) = trimmed.rfind(']') {
2773 if arr_end > arr_start {
2774 return trimmed[arr_start..=arr_end].to_string();
2775 }
2776 }
2777 }
2778
2779 trimmed.to_string()
2781}
2782
2783fn vm_output_to_value(output: std::process::Output) -> VmValue {
2788 let mut result = BTreeMap::new();
2789 result.insert(
2790 "stdout".to_string(),
2791 VmValue::String(Rc::from(
2792 String::from_utf8_lossy(&output.stdout).to_string().as_str(),
2793 )),
2794 );
2795 result.insert(
2796 "stderr".to_string(),
2797 VmValue::String(Rc::from(
2798 String::from_utf8_lossy(&output.stderr).to_string().as_str(),
2799 )),
2800 );
2801 result.insert(
2802 "status".to_string(),
2803 VmValue::Int(output.status.code().unwrap_or(-1) as i64),
2804 );
2805 result.insert(
2806 "success".to_string(),
2807 VmValue::Bool(output.status.success()),
2808 );
2809 VmValue::Dict(Rc::new(result))
2810}
2811
2812fn vm_civil_from_timestamp(total_secs: u64) -> (i64, i64, i64, i64, i64, i64, i64) {
2817 let days = total_secs / 86400;
2818 let time_of_day = total_secs % 86400;
2819 let hour = (time_of_day / 3600) as i64;
2820 let minute = ((time_of_day % 3600) / 60) as i64;
2821 let second = (time_of_day % 60) as i64;
2822
2823 let z = days as i64 + 719468;
2824 let era = if z >= 0 { z } else { z - 146096 } / 146097;
2825 let doe = (z - era * 146097) as u64;
2826 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2827 let y = yoe as i64 + era * 400;
2828 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2829 let mp = (5 * doy + 2) / 153;
2830 let d = (doy - (153 * mp + 2) / 5 + 1) as i64;
2831 let m = if mp < 10 { mp + 3 } else { mp - 9 } as i64;
2832 let y = if m <= 2 { y + 1 } else { y };
2833 let dow = ((days + 4) % 7) as i64;
2834
2835 (y, m, d, hour, minute, second, dow)
2836}
2837
2838pub(crate) static VM_MIN_LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
2843
2844#[derive(Clone)]
2845pub(crate) struct VmTraceContext {
2846 pub(crate) trace_id: String,
2847 pub(crate) span_id: String,
2848}
2849
2850thread_local! {
2851 pub(crate) static VM_TRACE_STACK: std::cell::RefCell<Vec<VmTraceContext>> = const { std::cell::RefCell::new(Vec::new()) };
2852}
2853
2854fn vm_level_to_u8(level: &str) -> Option<u8> {
2855 match level {
2856 "debug" => Some(0),
2857 "info" => Some(1),
2858 "warn" => Some(2),
2859 "error" => Some(3),
2860 _ => None,
2861 }
2862}
2863
2864fn vm_format_timestamp_utc() -> String {
2865 let now = std::time::SystemTime::now()
2866 .duration_since(std::time::UNIX_EPOCH)
2867 .unwrap_or_default();
2868 let total_secs = now.as_secs();
2869 let millis = now.subsec_millis();
2870
2871 let days = total_secs / 86400;
2872 let time_of_day = total_secs % 86400;
2873 let hour = time_of_day / 3600;
2874 let minute = (time_of_day % 3600) / 60;
2875 let second = time_of_day % 60;
2876
2877 let z = days as i64 + 719468;
2878 let era = if z >= 0 { z } else { z - 146096 } / 146097;
2879 let doe = (z - era * 146097) as u64;
2880 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2881 let y = yoe as i64 + era * 400;
2882 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2883 let mp = (5 * doy + 2) / 153;
2884 let d = doy - (153 * mp + 2) / 5 + 1;
2885 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2886 let y = if m <= 2 { y + 1 } else { y };
2887
2888 format!("{y:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
2889}
2890
2891pub(crate) fn vm_escape_json_str(s: &str) -> String {
2892 let mut out = String::with_capacity(s.len());
2893 for ch in s.chars() {
2894 match ch {
2895 '"' => out.push_str("\\\""),
2896 '\\' => out.push_str("\\\\"),
2897 '\n' => out.push_str("\\n"),
2898 '\r' => out.push_str("\\r"),
2899 '\t' => out.push_str("\\t"),
2900 c if c.is_control() => {
2901 out.push_str(&format!("\\u{:04x}", c as u32));
2902 }
2903 c => out.push(c),
2904 }
2905 }
2906 out
2907}
2908
2909fn vm_escape_json_str_quoted(s: &str) -> String {
2910 let mut out = String::with_capacity(s.len() + 2);
2911 out.push('"');
2912 out.push_str(&vm_escape_json_str(s));
2913 out.push('"');
2914 out
2915}
2916
2917fn vm_value_to_json_fragment(val: &VmValue) -> String {
2918 match val {
2919 VmValue::String(s) => vm_escape_json_str_quoted(s),
2920 VmValue::Int(n) => n.to_string(),
2921 VmValue::Float(n) => {
2922 if n.is_finite() {
2923 n.to_string()
2924 } else {
2925 "null".to_string()
2926 }
2927 }
2928 VmValue::Bool(b) => b.to_string(),
2929 VmValue::Nil => "null".to_string(),
2930 _ => vm_escape_json_str_quoted(&val.display()),
2931 }
2932}
2933
2934fn vm_build_log_line(level: &str, msg: &str, fields: Option<&BTreeMap<String, VmValue>>) -> String {
2935 let ts = vm_format_timestamp_utc();
2936 let mut parts: Vec<String> = Vec::new();
2937 parts.push(format!("\"ts\":{}", vm_escape_json_str_quoted(&ts)));
2938 parts.push(format!("\"level\":{}", vm_escape_json_str_quoted(level)));
2939 parts.push(format!("\"msg\":{}", vm_escape_json_str_quoted(msg)));
2940
2941 VM_TRACE_STACK.with(|stack| {
2942 if let Some(trace) = stack.borrow().last() {
2943 parts.push(format!(
2944 "\"trace_id\":{}",
2945 vm_escape_json_str_quoted(&trace.trace_id)
2946 ));
2947 parts.push(format!(
2948 "\"span_id\":{}",
2949 vm_escape_json_str_quoted(&trace.span_id)
2950 ));
2951 }
2952 });
2953
2954 if let Some(dict) = fields {
2955 for (k, v) in dict {
2956 parts.push(format!(
2957 "{}:{}",
2958 vm_escape_json_str_quoted(k),
2959 vm_value_to_json_fragment(v)
2960 ));
2961 }
2962 }
2963
2964 format!("{{{}}}\n", parts.join(","))
2965}
2966
2967fn vm_write_log(level: &str, level_num: u8, args: &[VmValue], out: &mut String) {
2968 if level_num < VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
2969 return;
2970 }
2971 let msg = args.first().map(|a| a.display()).unwrap_or_default();
2972 let fields = args.get(1).and_then(|v| {
2973 if let VmValue::Dict(d) = v {
2974 Some(&**d)
2975 } else {
2976 None
2977 }
2978 });
2979 let line = vm_build_log_line(level, &msg, fields);
2980 out.push_str(&line);
2981}
2982
2983fn vm_validate_registry(name: &str, dict: &BTreeMap<String, VmValue>) -> Result<(), VmError> {
2988 match dict.get("_type") {
2989 Some(VmValue::String(t)) if &**t == "tool_registry" => Ok(()),
2990 _ => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2991 "{name}: argument must be a tool registry (created with tool_registry())"
2992 ))))),
2993 }
2994}
2995
2996fn vm_get_tools(dict: &BTreeMap<String, VmValue>) -> &[VmValue] {
2997 match dict.get("tools") {
2998 Some(VmValue::List(list)) => list,
2999 _ => &[],
3000 }
3001}
3002
3003fn vm_format_parameters(params: Option<&VmValue>) -> String {
3004 match params {
3005 Some(VmValue::Dict(map)) if !map.is_empty() => {
3006 let mut pairs: Vec<(String, String)> =
3007 map.iter().map(|(k, v)| (k.clone(), v.display())).collect();
3008 pairs.sort_by(|a, b| a.0.cmp(&b.0));
3009 pairs
3010 .iter()
3011 .map(|(k, v)| format!("{k}: {v}"))
3012 .collect::<Vec<_>>()
3013 .join(", ")
3014 }
3015 _ => String::new(),
3016 }
3017}
3018
3019fn vm_build_empty_schema() -> BTreeMap<String, VmValue> {
3020 let mut schema = BTreeMap::new();
3021 schema.insert(
3022 "schema_version".to_string(),
3023 VmValue::String(Rc::from("harn-tools/1.0")),
3024 );
3025 schema.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
3026 schema
3027}
3028
3029fn vm_build_input_schema(
3030 params: Option<&VmValue>,
3031 components: Option<&BTreeMap<String, VmValue>>,
3032) -> VmValue {
3033 let mut schema = BTreeMap::new();
3034 schema.insert("type".to_string(), VmValue::String(Rc::from("object")));
3035
3036 let params_map = match params {
3037 Some(VmValue::Dict(map)) if !map.is_empty() => map,
3038 _ => {
3039 schema.insert(
3040 "properties".to_string(),
3041 VmValue::Dict(Rc::new(BTreeMap::new())),
3042 );
3043 return VmValue::Dict(Rc::new(schema));
3044 }
3045 };
3046
3047 let mut properties = BTreeMap::new();
3048 let mut required = Vec::new();
3049
3050 for (key, val) in params_map.iter() {
3051 let prop = vm_resolve_param_type(val, components);
3052 properties.insert(key.clone(), prop);
3053 required.push(VmValue::String(Rc::from(key.as_str())));
3054 }
3055
3056 schema.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
3057 if !required.is_empty() {
3058 required.sort_by_key(|a| a.display());
3059 schema.insert("required".to_string(), VmValue::List(Rc::new(required)));
3060 }
3061
3062 VmValue::Dict(Rc::new(schema))
3063}
3064
3065fn vm_resolve_param_type(val: &VmValue, components: Option<&BTreeMap<String, VmValue>>) -> VmValue {
3066 match val {
3067 VmValue::String(type_name) => {
3068 let json_type = vm_harn_type_to_json_schema(type_name);
3069 let mut prop = BTreeMap::new();
3070 prop.insert("type".to_string(), VmValue::String(Rc::from(json_type)));
3071 VmValue::Dict(Rc::new(prop))
3072 }
3073 VmValue::Dict(map) => {
3074 if let Some(VmValue::String(ref_name)) = map.get("$ref") {
3075 if let Some(comps) = components {
3076 if let Some(resolved) = comps.get(&**ref_name) {
3077 return resolved.clone();
3078 }
3079 }
3080 let mut prop = BTreeMap::new();
3081 prop.insert(
3082 "$ref".to_string(),
3083 VmValue::String(Rc::from(
3084 format!("#/components/schemas/{ref_name}").as_str(),
3085 )),
3086 );
3087 VmValue::Dict(Rc::new(prop))
3088 } else {
3089 VmValue::Dict(Rc::new((**map).clone()))
3090 }
3091 }
3092 _ => {
3093 let mut prop = BTreeMap::new();
3094 prop.insert("type".to_string(), VmValue::String(Rc::from("string")));
3095 VmValue::Dict(Rc::new(prop))
3096 }
3097 }
3098}
3099
3100fn vm_harn_type_to_json_schema(harn_type: &str) -> &str {
3101 match harn_type {
3102 "int" => "integer",
3103 "float" => "number",
3104 "bool" | "boolean" => "boolean",
3105 "list" | "array" => "array",
3106 "dict" | "object" => "object",
3107 _ => "string",
3108 }
3109}