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("__make_struct", |args, _out| {
305 let struct_name = args.first().map(|a| a.display()).unwrap_or_default();
306 let fields_dict = args.get(1).cloned().unwrap_or(VmValue::Nil);
307 match fields_dict {
308 VmValue::Dict(d) => Ok(VmValue::StructInstance {
309 struct_name,
310 fields: (*d).clone(),
311 }),
312 _ => Ok(VmValue::StructInstance {
313 struct_name,
314 fields: BTreeMap::new(),
315 }),
316 }
317 });
318
319 vm.register_builtin("base64_encode", |args, _out| {
321 let val = args.first().map(|a| a.display()).unwrap_or_default();
322 use base64::Engine;
323 Ok(VmValue::String(Rc::from(
324 base64::engine::general_purpose::STANDARD.encode(val.as_bytes()),
325 )))
326 });
327 vm.register_builtin("base64_decode", |args, _out| {
328 let val = args.first().map(|a| a.display()).unwrap_or_default();
329 use base64::Engine;
330 match base64::engine::general_purpose::STANDARD.decode(val.as_bytes()) {
331 Ok(bytes) => Ok(VmValue::String(Rc::from(
332 String::from_utf8_lossy(&bytes).into_owned(),
333 ))),
334 Err(e) => Err(VmError::Runtime(format!("base64 decode error: {e}"))),
335 }
336 });
337
338 vm.register_builtin("sha256", |args, _out| {
340 use sha2::Digest;
341 let val = args.first().map(|a| a.display()).unwrap_or_default();
342 let hash = sha2::Sha256::digest(val.as_bytes());
343 Ok(VmValue::String(Rc::from(format!("{hash:x}"))))
344 });
345 vm.register_builtin("md5", |args, _out| {
346 use md5::Digest;
347 let val = args.first().map(|a| a.display()).unwrap_or_default();
348 let hash = md5::Md5::digest(val.as_bytes());
349 Ok(VmValue::String(Rc::from(format!("{hash:x}"))))
350 });
351
352 vm.register_builtin("prompt_user", |args, out| {
353 let msg = args.first().map(|a| a.display()).unwrap_or_default();
354 out.push_str(&msg);
355 let mut input = String::new();
356 if std::io::stdin().lock().read_line(&mut input).is_ok() {
357 Ok(VmValue::String(Rc::from(input.trim_end())))
358 } else {
359 Ok(VmValue::Nil)
360 }
361 });
362
363 vm.register_builtin("abs", |args, _out| {
366 match args.first().unwrap_or(&VmValue::Nil) {
367 VmValue::Int(n) => Ok(VmValue::Int(n.wrapping_abs())),
368 VmValue::Float(n) => Ok(VmValue::Float(n.abs())),
369 _ => Ok(VmValue::Nil),
370 }
371 });
372
373 vm.register_builtin("min", |args, _out| {
374 if args.len() >= 2 {
375 match (&args[0], &args[1]) {
376 (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.min(y))),
377 (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.min(*y))),
378 (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).min(*y))),
379 (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.min(*y as f64))),
380 _ => Ok(VmValue::Nil),
381 }
382 } else {
383 Ok(VmValue::Nil)
384 }
385 });
386
387 vm.register_builtin("max", |args, _out| {
388 if args.len() >= 2 {
389 match (&args[0], &args[1]) {
390 (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.max(y))),
391 (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.max(*y))),
392 (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).max(*y))),
393 (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.max(*y as f64))),
394 _ => Ok(VmValue::Nil),
395 }
396 } else {
397 Ok(VmValue::Nil)
398 }
399 });
400
401 vm.register_builtin("floor", |args, _out| {
402 match args.first().unwrap_or(&VmValue::Nil) {
403 VmValue::Float(n) => Ok(VmValue::Int(n.floor() as i64)),
404 VmValue::Int(n) => Ok(VmValue::Int(*n)),
405 _ => Ok(VmValue::Nil),
406 }
407 });
408
409 vm.register_builtin("ceil", |args, _out| {
410 match args.first().unwrap_or(&VmValue::Nil) {
411 VmValue::Float(n) => Ok(VmValue::Int(n.ceil() as i64)),
412 VmValue::Int(n) => Ok(VmValue::Int(*n)),
413 _ => Ok(VmValue::Nil),
414 }
415 });
416
417 vm.register_builtin("round", |args, _out| {
418 match args.first().unwrap_or(&VmValue::Nil) {
419 VmValue::Float(n) => Ok(VmValue::Int(n.round() as i64)),
420 VmValue::Int(n) => Ok(VmValue::Int(*n)),
421 _ => Ok(VmValue::Nil),
422 }
423 });
424
425 vm.register_builtin("sqrt", |args, _out| {
426 match args.first().unwrap_or(&VmValue::Nil) {
427 VmValue::Float(n) => Ok(VmValue::Float(n.sqrt())),
428 VmValue::Int(n) => Ok(VmValue::Float((*n as f64).sqrt())),
429 _ => Ok(VmValue::Nil),
430 }
431 });
432
433 vm.register_builtin("pow", |args, _out| {
434 if args.len() >= 2 {
435 match (&args[0], &args[1]) {
436 (VmValue::Int(base), VmValue::Int(exp)) => {
437 if *exp >= 0 && *exp <= u32::MAX as i64 {
438 Ok(VmValue::Int(base.wrapping_pow(*exp as u32)))
439 } else {
440 Ok(VmValue::Float((*base as f64).powf(*exp as f64)))
441 }
442 }
443 (VmValue::Float(base), VmValue::Int(exp)) => {
444 if *exp >= i32::MIN as i64 && *exp <= i32::MAX as i64 {
445 Ok(VmValue::Float(base.powi(*exp as i32)))
446 } else {
447 Ok(VmValue::Float(base.powf(*exp as f64)))
448 }
449 }
450 (VmValue::Int(base), VmValue::Float(exp)) => {
451 Ok(VmValue::Float((*base as f64).powf(*exp)))
452 }
453 (VmValue::Float(base), VmValue::Float(exp)) => Ok(VmValue::Float(base.powf(*exp))),
454 _ => Ok(VmValue::Nil),
455 }
456 } else {
457 Ok(VmValue::Nil)
458 }
459 });
460
461 vm.register_builtin("random", |_args, _out| {
462 use rand::Rng;
463 let val: f64 = rand::thread_rng().gen();
464 Ok(VmValue::Float(val))
465 });
466
467 vm.register_builtin("random_int", |args, _out| {
468 use rand::Rng;
469 if args.len() >= 2 {
470 let min = args[0].as_int().unwrap_or(0);
471 let max = args[1].as_int().unwrap_or(0);
472 if min <= max {
473 let val = rand::thread_rng().gen_range(min..=max);
474 return Ok(VmValue::Int(val));
475 }
476 }
477 Ok(VmValue::Nil)
478 });
479
480 vm.register_builtin("sin", |args, _out| {
483 let n = match args.first().unwrap_or(&VmValue::Nil) {
484 VmValue::Float(n) => *n,
485 VmValue::Int(n) => *n as f64,
486 _ => return Ok(VmValue::Nil),
487 };
488 Ok(VmValue::Float(n.sin()))
489 });
490
491 vm.register_builtin("cos", |args, _out| {
492 let n = match args.first().unwrap_or(&VmValue::Nil) {
493 VmValue::Float(n) => *n,
494 VmValue::Int(n) => *n as f64,
495 _ => return Ok(VmValue::Nil),
496 };
497 Ok(VmValue::Float(n.cos()))
498 });
499
500 vm.register_builtin("tan", |args, _out| {
501 let n = match args.first().unwrap_or(&VmValue::Nil) {
502 VmValue::Float(n) => *n,
503 VmValue::Int(n) => *n as f64,
504 _ => return Ok(VmValue::Nil),
505 };
506 Ok(VmValue::Float(n.tan()))
507 });
508
509 vm.register_builtin("asin", |args, _out| {
510 let n = match args.first().unwrap_or(&VmValue::Nil) {
511 VmValue::Float(n) => *n,
512 VmValue::Int(n) => *n as f64,
513 _ => return Ok(VmValue::Nil),
514 };
515 Ok(VmValue::Float(n.asin()))
516 });
517
518 vm.register_builtin("acos", |args, _out| {
519 let n = match args.first().unwrap_or(&VmValue::Nil) {
520 VmValue::Float(n) => *n,
521 VmValue::Int(n) => *n as f64,
522 _ => return Ok(VmValue::Nil),
523 };
524 Ok(VmValue::Float(n.acos()))
525 });
526
527 vm.register_builtin("atan", |args, _out| {
528 let n = match args.first().unwrap_or(&VmValue::Nil) {
529 VmValue::Float(n) => *n,
530 VmValue::Int(n) => *n as f64,
531 _ => return Ok(VmValue::Nil),
532 };
533 Ok(VmValue::Float(n.atan()))
534 });
535
536 vm.register_builtin("atan2", |args, _out| {
537 if args.len() >= 2 {
538 let y = match &args[0] {
539 VmValue::Float(n) => *n,
540 VmValue::Int(n) => *n as f64,
541 _ => return Ok(VmValue::Nil),
542 };
543 let x = match &args[1] {
544 VmValue::Float(n) => *n,
545 VmValue::Int(n) => *n as f64,
546 _ => return Ok(VmValue::Nil),
547 };
548 Ok(VmValue::Float(y.atan2(x)))
549 } else {
550 Ok(VmValue::Nil)
551 }
552 });
553
554 vm.register_builtin("log2", |args, _out| {
555 let n = match args.first().unwrap_or(&VmValue::Nil) {
556 VmValue::Float(n) => *n,
557 VmValue::Int(n) => *n as f64,
558 _ => return Ok(VmValue::Nil),
559 };
560 Ok(VmValue::Float(n.log2()))
561 });
562
563 vm.register_builtin("log10", |args, _out| {
564 let n = match args.first().unwrap_or(&VmValue::Nil) {
565 VmValue::Float(n) => *n,
566 VmValue::Int(n) => *n as f64,
567 _ => return Ok(VmValue::Nil),
568 };
569 Ok(VmValue::Float(n.log10()))
570 });
571
572 vm.register_builtin("ln", |args, _out| {
573 let n = match args.first().unwrap_or(&VmValue::Nil) {
574 VmValue::Float(n) => *n,
575 VmValue::Int(n) => *n as f64,
576 _ => return Ok(VmValue::Nil),
577 };
578 Ok(VmValue::Float(n.ln()))
579 });
580
581 vm.register_builtin("exp", |args, _out| {
582 let n = match args.first().unwrap_or(&VmValue::Nil) {
583 VmValue::Float(n) => *n,
584 VmValue::Int(n) => *n as f64,
585 _ => return Ok(VmValue::Nil),
586 };
587 Ok(VmValue::Float(n.exp()))
588 });
589
590 vm.register_builtin("sign", |args, _out| {
591 match args.first().unwrap_or(&VmValue::Nil) {
592 VmValue::Int(n) => Ok(VmValue::Int(n.signum())),
593 VmValue::Float(n) => {
594 if n.is_nan() {
595 Ok(VmValue::Float(f64::NAN))
596 } else if *n == 0.0 {
597 Ok(VmValue::Int(0))
598 } else if *n > 0.0 {
599 Ok(VmValue::Int(1))
600 } else {
601 Ok(VmValue::Int(-1))
602 }
603 }
604 _ => Ok(VmValue::Nil),
605 }
606 });
607
608 vm.register_builtin("pi", |_args, _out| Ok(VmValue::Float(std::f64::consts::PI)));
609
610 vm.register_builtin("e", |_args, _out| Ok(VmValue::Float(std::f64::consts::E)));
611
612 vm.register_builtin("is_nan", |args, _out| {
613 match args.first().unwrap_or(&VmValue::Nil) {
614 VmValue::Float(n) => Ok(VmValue::Bool(n.is_nan())),
615 _ => Ok(VmValue::Bool(false)),
616 }
617 });
618
619 vm.register_builtin("is_infinite", |args, _out| {
620 match args.first().unwrap_or(&VmValue::Nil) {
621 VmValue::Float(n) => Ok(VmValue::Bool(n.is_infinite())),
622 _ => Ok(VmValue::Bool(false)),
623 }
624 });
625
626 vm.register_builtin("set", |args, _out| {
630 let mut items: Vec<VmValue> = Vec::new();
631 for arg in args {
632 match arg {
633 VmValue::List(list) => {
634 for v in list.iter() {
635 if !items.iter().any(|x| values_equal(x, v)) {
636 items.push(v.clone());
637 }
638 }
639 }
640 VmValue::Set(s) => {
641 for v in s.iter() {
642 if !items.iter().any(|x| values_equal(x, v)) {
643 items.push(v.clone());
644 }
645 }
646 }
647 other => {
648 if !items.iter().any(|x| values_equal(x, other)) {
649 items.push(other.clone());
650 }
651 }
652 }
653 }
654 Ok(VmValue::Set(Rc::new(items)))
655 });
656
657 vm.register_builtin("set_add", |args, _out| {
659 let s = match args.first() {
660 Some(VmValue::Set(s)) => s.clone(),
661 _ => {
662 return Err(VmError::Thrown(VmValue::String(Rc::from(
663 "set_add: first argument must be a set",
664 ))));
665 }
666 };
667 let val = args.get(1).cloned().unwrap_or(VmValue::Nil);
668 let mut items: Vec<VmValue> = (*s).clone();
669 if !items.iter().any(|x| values_equal(x, &val)) {
670 items.push(val);
671 }
672 Ok(VmValue::Set(Rc::new(items)))
673 });
674
675 vm.register_builtin("set_remove", |args, _out| {
677 let s = match args.first() {
678 Some(VmValue::Set(s)) => s.clone(),
679 _ => {
680 return Err(VmError::Thrown(VmValue::String(Rc::from(
681 "set_remove: first argument must be a set",
682 ))));
683 }
684 };
685 let val = args.get(1).cloned().unwrap_or(VmValue::Nil);
686 let items: Vec<VmValue> = s
687 .iter()
688 .filter(|x| !values_equal(x, &val))
689 .cloned()
690 .collect();
691 Ok(VmValue::Set(Rc::new(items)))
692 });
693
694 vm.register_builtin("set_contains", |args, _out| {
696 let s = match args.first() {
697 Some(VmValue::Set(s)) => s,
698 _ => return Ok(VmValue::Bool(false)),
699 };
700 let val = args.get(1).unwrap_or(&VmValue::Nil);
701 Ok(VmValue::Bool(s.iter().any(|x| values_equal(x, val))))
702 });
703
704 vm.register_builtin("set_union", |args, _out| {
706 let a = match args.first() {
707 Some(VmValue::Set(s)) => s,
708 _ => {
709 return Err(VmError::Thrown(VmValue::String(Rc::from(
710 "set_union: arguments must be sets",
711 ))));
712 }
713 };
714 let b = match args.get(1) {
715 Some(VmValue::Set(s)) => s,
716 _ => {
717 return Err(VmError::Thrown(VmValue::String(Rc::from(
718 "set_union: arguments must be sets",
719 ))));
720 }
721 };
722 let mut items: Vec<VmValue> = (**a).clone();
723 for v in b.iter() {
724 if !items.iter().any(|x| values_equal(x, v)) {
725 items.push(v.clone());
726 }
727 }
728 Ok(VmValue::Set(Rc::new(items)))
729 });
730
731 vm.register_builtin("set_intersect", |args, _out| {
733 let a = match args.first() {
734 Some(VmValue::Set(s)) => s,
735 _ => {
736 return Err(VmError::Thrown(VmValue::String(Rc::from(
737 "set_intersect: arguments must be sets",
738 ))));
739 }
740 };
741 let b = match args.get(1) {
742 Some(VmValue::Set(s)) => s,
743 _ => {
744 return Err(VmError::Thrown(VmValue::String(Rc::from(
745 "set_intersect: arguments must be sets",
746 ))));
747 }
748 };
749 let items: Vec<VmValue> = a
750 .iter()
751 .filter(|x| b.iter().any(|y| values_equal(x, y)))
752 .cloned()
753 .collect();
754 Ok(VmValue::Set(Rc::new(items)))
755 });
756
757 vm.register_builtin("set_difference", |args, _out| {
759 let a = match args.first() {
760 Some(VmValue::Set(s)) => s,
761 _ => {
762 return Err(VmError::Thrown(VmValue::String(Rc::from(
763 "set_difference: arguments must be sets",
764 ))));
765 }
766 };
767 let b = match args.get(1) {
768 Some(VmValue::Set(s)) => s,
769 _ => {
770 return Err(VmError::Thrown(VmValue::String(Rc::from(
771 "set_difference: arguments must be sets",
772 ))));
773 }
774 };
775 let items: Vec<VmValue> = a
776 .iter()
777 .filter(|x| !b.iter().any(|y| values_equal(x, y)))
778 .cloned()
779 .collect();
780 Ok(VmValue::Set(Rc::new(items)))
781 });
782
783 vm.register_builtin("to_list", |args, _out| {
785 match args.first().unwrap_or(&VmValue::Nil) {
786 VmValue::Set(s) => Ok(VmValue::List(Rc::new(s.to_vec()))),
787 VmValue::List(l) => Ok(VmValue::List(l.clone())),
788 other => Ok(VmValue::List(Rc::new(vec![other.clone()]))),
789 }
790 });
791
792 vm.register_builtin("assert", |args, _out| {
795 let condition = args.first().unwrap_or(&VmValue::Nil);
796 if !condition.is_truthy() {
797 let msg = args
798 .get(1)
799 .map(|a| a.display())
800 .unwrap_or_else(|| "Assertion failed".to_string());
801 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
802 }
803 Ok(VmValue::Nil)
804 });
805
806 vm.register_builtin("assert_eq", |args, _out| {
807 if args.len() >= 2 {
808 if !values_equal(&args[0], &args[1]) {
809 let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
810 format!(
811 "Assertion failed: expected {}, got {}",
812 args[1].display(),
813 args[0].display()
814 )
815 });
816 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
817 }
818 Ok(VmValue::Nil)
819 } else {
820 Err(VmError::Thrown(VmValue::String(Rc::from(
821 "assert_eq requires at least 2 arguments",
822 ))))
823 }
824 });
825
826 vm.register_builtin("assert_ne", |args, _out| {
827 if args.len() >= 2 {
828 if values_equal(&args[0], &args[1]) {
829 let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
830 format!(
831 "Assertion failed: values should not be equal: {}",
832 args[0].display()
833 )
834 });
835 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
836 }
837 Ok(VmValue::Nil)
838 } else {
839 Err(VmError::Thrown(VmValue::String(Rc::from(
840 "assert_ne requires at least 2 arguments",
841 ))))
842 }
843 });
844
845 vm.register_builtin("__range__", |args, _out| {
846 let start = args.first().and_then(|a| a.as_int()).unwrap_or(0);
847 let end = args.get(1).and_then(|a| a.as_int()).unwrap_or(0);
848 let inclusive = args.get(2).map(|a| a.is_truthy()).unwrap_or(false);
849 let items: Vec<VmValue> = if inclusive {
850 (start..=end).map(VmValue::Int).collect()
851 } else {
852 (start..end).map(VmValue::Int).collect()
853 };
854 Ok(VmValue::List(Rc::new(items)))
855 });
856
857 vm.register_builtin("file_exists", |args, _out| {
862 let path = args.first().map(|a| a.display()).unwrap_or_default();
863 Ok(VmValue::Bool(std::path::Path::new(&path).exists()))
864 });
865
866 vm.register_builtin("delete_file", |args, _out| {
867 let path = args.first().map(|a| a.display()).unwrap_or_default();
868 let p = std::path::Path::new(&path);
869 if p.is_dir() {
870 std::fs::remove_dir_all(&path).map_err(|e| {
871 VmError::Thrown(VmValue::String(Rc::from(format!(
872 "Failed to delete directory {path}: {e}"
873 ))))
874 })?;
875 } else {
876 std::fs::remove_file(&path).map_err(|e| {
877 VmError::Thrown(VmValue::String(Rc::from(format!(
878 "Failed to delete file {path}: {e}"
879 ))))
880 })?;
881 }
882 Ok(VmValue::Nil)
883 });
884
885 vm.register_builtin("append_file", |args, _out| {
886 use std::io::Write;
887 if args.len() >= 2 {
888 let path = args[0].display();
889 let content = args[1].display();
890 let mut file = std::fs::OpenOptions::new()
891 .append(true)
892 .create(true)
893 .open(&path)
894 .map_err(|e| {
895 VmError::Thrown(VmValue::String(Rc::from(format!(
896 "Failed to open file {path}: {e}"
897 ))))
898 })?;
899 file.write_all(content.as_bytes()).map_err(|e| {
900 VmError::Thrown(VmValue::String(Rc::from(format!(
901 "Failed to append to file {path}: {e}"
902 ))))
903 })?;
904 }
905 Ok(VmValue::Nil)
906 });
907
908 vm.register_builtin("list_dir", |args, _out| {
909 let path = args
910 .first()
911 .map(|a| a.display())
912 .unwrap_or_else(|| ".".to_string());
913 let entries = std::fs::read_dir(&path).map_err(|e| {
914 VmError::Thrown(VmValue::String(Rc::from(format!(
915 "Failed to list directory {path}: {e}"
916 ))))
917 })?;
918 let mut result = Vec::new();
919 for entry in entries {
920 let entry =
921 entry.map_err(|e| VmError::Thrown(VmValue::String(Rc::from(e.to_string()))))?;
922 let name = entry.file_name().to_string_lossy().to_string();
923 result.push(VmValue::String(Rc::from(name.as_str())));
924 }
925 result.sort_by_key(|a| a.display());
926 Ok(VmValue::List(Rc::new(result)))
927 });
928
929 vm.register_builtin("mkdir", |args, _out| {
930 let path = args.first().map(|a| a.display()).unwrap_or_default();
931 std::fs::create_dir_all(&path).map_err(|e| {
932 VmError::Thrown(VmValue::String(Rc::from(format!(
933 "Failed to create directory {path}: {e}"
934 ))))
935 })?;
936 Ok(VmValue::Nil)
937 });
938
939 vm.register_builtin("path_join", |args, _out| {
940 let mut path = std::path::PathBuf::new();
941 for arg in args {
942 path.push(arg.display());
943 }
944 Ok(VmValue::String(Rc::from(
945 path.to_string_lossy().to_string().as_str(),
946 )))
947 });
948
949 vm.register_builtin("copy_file", |args, _out| {
950 if args.len() >= 2 {
951 let src = args[0].display();
952 let dst = args[1].display();
953 std::fs::copy(&src, &dst).map_err(|e| {
954 VmError::Thrown(VmValue::String(Rc::from(format!(
955 "Failed to copy {src} to {dst}: {e}"
956 ))))
957 })?;
958 }
959 Ok(VmValue::Nil)
960 });
961
962 vm.register_builtin("temp_dir", |_args, _out| {
963 Ok(VmValue::String(Rc::from(
964 std::env::temp_dir().to_string_lossy().to_string().as_str(),
965 )))
966 });
967
968 vm.register_builtin("stat", |args, _out| {
969 let path = args.first().map(|a| a.display()).unwrap_or_default();
970 let metadata = std::fs::metadata(&path).map_err(|e| {
971 VmError::Thrown(VmValue::String(Rc::from(format!(
972 "Failed to stat {path}: {e}"
973 ))))
974 })?;
975 let mut info = BTreeMap::new();
976 info.insert("size".to_string(), VmValue::Int(metadata.len() as i64));
977 info.insert("is_file".to_string(), VmValue::Bool(metadata.is_file()));
978 info.insert("is_dir".to_string(), VmValue::Bool(metadata.is_dir()));
979 info.insert(
980 "readonly".to_string(),
981 VmValue::Bool(metadata.permissions().readonly()),
982 );
983 if let Ok(modified) = metadata.modified() {
984 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
985 info.insert("modified".to_string(), VmValue::Float(dur.as_secs_f64()));
986 }
987 }
988 Ok(VmValue::Dict(Rc::new(info)))
989 });
990
991 vm.register_builtin("exec", |args, _out| {
996 if args.is_empty() {
997 return Err(VmError::Thrown(VmValue::String(Rc::from(
998 "exec: command is required",
999 ))));
1000 }
1001 let cmd = args[0].display();
1002 let cmd_args: Vec<String> = args[1..].iter().map(|a| a.display()).collect();
1003 let output = std::process::Command::new(&cmd)
1004 .args(&cmd_args)
1005 .output()
1006 .map_err(|e| VmError::Thrown(VmValue::String(Rc::from(format!("exec failed: {e}")))))?;
1007 Ok(vm_output_to_value(output))
1008 });
1009
1010 vm.register_builtin("shell", |args, _out| {
1011 let cmd = args.first().map(|a| a.display()).unwrap_or_default();
1012 if cmd.is_empty() {
1013 return Err(VmError::Thrown(VmValue::String(Rc::from(
1014 "shell: command string is required",
1015 ))));
1016 }
1017 let shell = if cfg!(target_os = "windows") {
1018 "cmd"
1019 } else {
1020 "sh"
1021 };
1022 let flag = if cfg!(target_os = "windows") {
1023 "/C"
1024 } else {
1025 "-c"
1026 };
1027 let output = std::process::Command::new(shell)
1028 .arg(flag)
1029 .arg(&cmd)
1030 .output()
1031 .map_err(|e| {
1032 VmError::Thrown(VmValue::String(Rc::from(format!("shell failed: {e}"))))
1033 })?;
1034 Ok(vm_output_to_value(output))
1035 });
1036
1037 vm.register_builtin("date_now", |_args, _out| {
1042 use std::time::{SystemTime, UNIX_EPOCH};
1043 let now = SystemTime::now()
1044 .duration_since(UNIX_EPOCH)
1045 .unwrap_or_default();
1046 let total_secs = now.as_secs();
1047 let (y, m, d, hour, minute, second, dow) = vm_civil_from_timestamp(total_secs);
1048 let mut result = BTreeMap::new();
1049 result.insert("year".to_string(), VmValue::Int(y));
1050 result.insert("month".to_string(), VmValue::Int(m));
1051 result.insert("day".to_string(), VmValue::Int(d));
1052 result.insert("hour".to_string(), VmValue::Int(hour));
1053 result.insert("minute".to_string(), VmValue::Int(minute));
1054 result.insert("second".to_string(), VmValue::Int(second));
1055 result.insert("weekday".to_string(), VmValue::Int(dow));
1056 result.insert("timestamp".to_string(), VmValue::Float(now.as_secs_f64()));
1057 Ok(VmValue::Dict(Rc::new(result)))
1058 });
1059
1060 vm.register_builtin("date_format", |args, _out| {
1061 let ts = match args.first() {
1062 Some(VmValue::Float(f)) => *f,
1063 Some(VmValue::Int(n)) => *n as f64,
1064 Some(VmValue::Dict(map)) => map
1065 .get("timestamp")
1066 .and_then(|v| match v {
1067 VmValue::Float(f) => Some(*f),
1068 VmValue::Int(n) => Some(*n as f64),
1069 _ => None,
1070 })
1071 .unwrap_or(0.0),
1072 _ => 0.0,
1073 };
1074 let fmt = args
1075 .get(1)
1076 .map(|a| a.display())
1077 .unwrap_or_else(|| "%Y-%m-%d %H:%M:%S".to_string());
1078
1079 let (y, m, d, hour, minute, second, _dow) = vm_civil_from_timestamp(ts as u64);
1080
1081 let result = fmt
1082 .replace("%Y", &format!("{y:04}"))
1083 .replace("%m", &format!("{m:02}"))
1084 .replace("%d", &format!("{d:02}"))
1085 .replace("%H", &format!("{hour:02}"))
1086 .replace("%M", &format!("{minute:02}"))
1087 .replace("%S", &format!("{second:02}"));
1088
1089 Ok(VmValue::String(Rc::from(result.as_str())))
1090 });
1091
1092 vm.register_builtin("date_parse", |args, _out| {
1093 let s = args.first().map(|a| a.display()).unwrap_or_default();
1094 let parts: Vec<&str> = s.split(|c: char| !c.is_ascii_digit()).collect();
1095 let parts: Vec<i64> = parts.iter().filter_map(|p| p.parse().ok()).collect();
1096 if parts.len() < 3 {
1097 return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1098 "Cannot parse date: {s}"
1099 )))));
1100 }
1101 let (y, m, d) = (parts[0], parts[1], parts[2]);
1102 let hour = parts.get(3).copied().unwrap_or(0);
1103 let minute = parts.get(4).copied().unwrap_or(0);
1104 let second = parts.get(5).copied().unwrap_or(0);
1105
1106 let (y_adj, m_adj) = if m <= 2 {
1107 (y - 1, (m + 9) as u64)
1108 } else {
1109 (y, (m - 3) as u64)
1110 };
1111 let era = if y_adj >= 0 { y_adj } else { y_adj - 399 } / 400;
1112 let yoe = (y_adj - era * 400) as u64;
1113 let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
1114 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
1115 let days = era * 146097 + doe as i64 - 719468;
1116 let ts = days * 86400 + hour * 3600 + minute * 60 + second;
1117 Ok(VmValue::Float(ts as f64))
1118 });
1119
1120 vm.register_builtin("format", |args, _out| {
1125 let template = args.first().map(|a| a.display()).unwrap_or_default();
1126 let mut result = String::with_capacity(template.len());
1127 let mut arg_iter = args.iter().skip(1);
1128 let mut rest = template.as_str();
1129 while let Some(pos) = rest.find("{}") {
1130 result.push_str(&rest[..pos]);
1131 if let Some(arg) = arg_iter.next() {
1132 result.push_str(&arg.display());
1133 } else {
1134 result.push_str("{}");
1135 }
1136 rest = &rest[pos + 2..];
1137 }
1138 result.push_str(rest);
1139 Ok(VmValue::String(Rc::from(result.as_str())))
1140 });
1141
1142 vm.register_builtin("trim", |args, _out| {
1147 let s = args.first().map(|a| a.display()).unwrap_or_default();
1148 Ok(VmValue::String(Rc::from(s.trim())))
1149 });
1150
1151 vm.register_builtin("lowercase", |args, _out| {
1152 let s = args.first().map(|a| a.display()).unwrap_or_default();
1153 Ok(VmValue::String(Rc::from(s.to_lowercase().as_str())))
1154 });
1155
1156 vm.register_builtin("uppercase", |args, _out| {
1157 let s = args.first().map(|a| a.display()).unwrap_or_default();
1158 Ok(VmValue::String(Rc::from(s.to_uppercase().as_str())))
1159 });
1160
1161 vm.register_builtin("split", |args, _out| {
1162 let s = args.first().map(|a| a.display()).unwrap_or_default();
1163 let sep = args
1164 .get(1)
1165 .map(|a| a.display())
1166 .unwrap_or_else(|| " ".to_string());
1167 let parts: Vec<VmValue> = s
1168 .split(&sep)
1169 .map(|p| VmValue::String(Rc::from(p)))
1170 .collect();
1171 Ok(VmValue::List(Rc::new(parts)))
1172 });
1173
1174 vm.register_builtin("starts_with", |args, _out| {
1175 let s = args.first().map(|a| a.display()).unwrap_or_default();
1176 let prefix = args.get(1).map(|a| a.display()).unwrap_or_default();
1177 Ok(VmValue::Bool(s.starts_with(&prefix)))
1178 });
1179
1180 vm.register_builtin("ends_with", |args, _out| {
1181 let s = args.first().map(|a| a.display()).unwrap_or_default();
1182 let suffix = args.get(1).map(|a| a.display()).unwrap_or_default();
1183 Ok(VmValue::Bool(s.ends_with(&suffix)))
1184 });
1185
1186 vm.register_builtin("contains", |args, _out| {
1187 match args.first().unwrap_or(&VmValue::Nil) {
1188 VmValue::String(s) => {
1189 let substr = args.get(1).map(|a| a.display()).unwrap_or_default();
1190 Ok(VmValue::Bool(s.contains(&substr)))
1191 }
1192 VmValue::List(items) => {
1193 let target = args.get(1).unwrap_or(&VmValue::Nil);
1194 Ok(VmValue::Bool(
1195 items.iter().any(|item| values_equal(item, target)),
1196 ))
1197 }
1198 _ => Ok(VmValue::Bool(false)),
1199 }
1200 });
1201
1202 vm.register_builtin("replace", |args, _out| {
1203 let s = args.first().map(|a| a.display()).unwrap_or_default();
1204 let old = args.get(1).map(|a| a.display()).unwrap_or_default();
1205 let new = args.get(2).map(|a| a.display()).unwrap_or_default();
1206 Ok(VmValue::String(Rc::from(s.replace(&old, &new).as_str())))
1207 });
1208
1209 vm.register_builtin("join", |args, _out| {
1210 let sep = args.get(1).map(|a| a.display()).unwrap_or_default();
1211 match args.first() {
1212 Some(VmValue::List(items)) => {
1213 let parts: Vec<String> = items.iter().map(|v| v.display()).collect();
1214 Ok(VmValue::String(Rc::from(parts.join(&sep).as_str())))
1215 }
1216 _ => Ok(VmValue::String(Rc::from(""))),
1217 }
1218 });
1219
1220 vm.register_builtin("len", |args, _out| {
1221 match args.first().unwrap_or(&VmValue::Nil) {
1222 VmValue::String(s) => Ok(VmValue::Int(s.len() as i64)),
1223 VmValue::List(items) => Ok(VmValue::Int(items.len() as i64)),
1224 VmValue::Dict(map) => Ok(VmValue::Int(map.len() as i64)),
1225 VmValue::Set(s) => Ok(VmValue::Int(s.len() as i64)),
1226 _ => Ok(VmValue::Int(0)),
1227 }
1228 });
1229
1230 vm.register_builtin("substring", |args, _out| {
1231 let s = args.first().map(|a| a.display()).unwrap_or_default();
1232 let start = args.get(1).and_then(|a| a.as_int()).unwrap_or(0) as usize;
1233 let start = start.min(s.len());
1234 match args.get(2).and_then(|a| a.as_int()) {
1235 Some(length) => {
1236 let length = (length as usize).min(s.len() - start);
1237 Ok(VmValue::String(Rc::from(&s[start..start + length])))
1238 }
1239 None => Ok(VmValue::String(Rc::from(&s[start..]))),
1240 }
1241 });
1242
1243 vm.register_builtin("dirname", |args, _out| {
1248 let path = args.first().map(|a| a.display()).unwrap_or_default();
1249 let p = std::path::Path::new(&path);
1250 match p.parent() {
1251 Some(parent) => Ok(VmValue::String(Rc::from(parent.to_string_lossy().as_ref()))),
1252 None => Ok(VmValue::String(Rc::from(""))),
1253 }
1254 });
1255
1256 vm.register_builtin("basename", |args, _out| {
1257 let path = args.first().map(|a| a.display()).unwrap_or_default();
1258 let p = std::path::Path::new(&path);
1259 match p.file_name() {
1260 Some(name) => Ok(VmValue::String(Rc::from(name.to_string_lossy().as_ref()))),
1261 None => Ok(VmValue::String(Rc::from(""))),
1262 }
1263 });
1264
1265 vm.register_builtin("extname", |args, _out| {
1266 let path = args.first().map(|a| a.display()).unwrap_or_default();
1267 let p = std::path::Path::new(&path);
1268 match p.extension() {
1269 Some(ext) => Ok(VmValue::String(Rc::from(
1270 format!(".{}", ext.to_string_lossy()).as_str(),
1271 ))),
1272 None => Ok(VmValue::String(Rc::from(""))),
1273 }
1274 });
1275
1276 vm.register_builtin("render", |args, _out| {
1281 let path = args.first().map(|a| a.display()).unwrap_or_default();
1282 let template = std::fs::read_to_string(&path).map_err(|e| {
1283 VmError::Thrown(VmValue::String(Rc::from(format!(
1284 "Failed to read template {path}: {e}"
1285 ))))
1286 })?;
1287 if let Some(bindings) = args.get(1).and_then(|a| a.as_dict()) {
1288 let mut result = template;
1289 for (key, val) in bindings.iter() {
1290 result = result.replace(&format!("{{{{{key}}}}}"), &val.display());
1291 }
1292 Ok(VmValue::String(Rc::from(result)))
1293 } else {
1294 Ok(VmValue::String(Rc::from(template)))
1295 }
1296 });
1297
1298 vm.register_builtin("log_debug", |args, out| {
1303 vm_write_log("debug", 0, args, out);
1304 Ok(VmValue::Nil)
1305 });
1306
1307 vm.register_builtin("log_info", |args, out| {
1308 vm_write_log("info", 1, args, out);
1309 Ok(VmValue::Nil)
1310 });
1311
1312 vm.register_builtin("log_warn", |args, out| {
1313 vm_write_log("warn", 2, args, out);
1314 Ok(VmValue::Nil)
1315 });
1316
1317 vm.register_builtin("log_error", |args, out| {
1318 vm_write_log("error", 3, args, out);
1319 Ok(VmValue::Nil)
1320 });
1321
1322 vm.register_builtin("log_set_level", |args, _out| {
1323 let level_str = args.first().map(|a| a.display()).unwrap_or_default();
1324 match vm_level_to_u8(&level_str) {
1325 Some(n) => {
1326 VM_MIN_LOG_LEVEL.store(n, Ordering::Relaxed);
1327 Ok(VmValue::Nil)
1328 }
1329 None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1330 "log_set_level: invalid level '{}'. Expected debug, info, warn, or error",
1331 level_str
1332 ))))),
1333 }
1334 });
1335
1336 vm.register_builtin("trace_start", |args, _out| {
1341 use rand::Rng;
1342 let name = args.first().map(|a| a.display()).unwrap_or_default();
1343 let trace_id = VM_TRACE_STACK.with(|stack| {
1344 stack
1345 .borrow()
1346 .last()
1347 .map(|t| t.trace_id.clone())
1348 .unwrap_or_else(|| {
1349 let val: u32 = rand::thread_rng().gen();
1350 format!("{val:08x}")
1351 })
1352 });
1353 let span_id = {
1354 let val: u32 = rand::thread_rng().gen();
1355 format!("{val:08x}")
1356 };
1357 let start_ms = std::time::SystemTime::now()
1358 .duration_since(std::time::UNIX_EPOCH)
1359 .unwrap_or_default()
1360 .as_millis() as i64;
1361
1362 VM_TRACE_STACK.with(|stack| {
1363 stack.borrow_mut().push(VmTraceContext {
1364 trace_id: trace_id.clone(),
1365 span_id: span_id.clone(),
1366 });
1367 });
1368
1369 let mut span = BTreeMap::new();
1370 span.insert(
1371 "trace_id".to_string(),
1372 VmValue::String(Rc::from(trace_id.as_str())),
1373 );
1374 span.insert(
1375 "span_id".to_string(),
1376 VmValue::String(Rc::from(span_id.as_str())),
1377 );
1378 span.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1379 span.insert("start_ms".to_string(), VmValue::Int(start_ms));
1380 Ok(VmValue::Dict(Rc::new(span)))
1381 });
1382
1383 vm.register_builtin("trace_end", |args, out| {
1384 let span = match args.first() {
1385 Some(VmValue::Dict(d)) => d,
1386 _ => {
1387 return Err(VmError::Thrown(VmValue::String(Rc::from(
1388 "trace_end: argument must be a span dict from trace_start",
1389 ))));
1390 }
1391 };
1392
1393 let end_ms = std::time::SystemTime::now()
1394 .duration_since(std::time::UNIX_EPOCH)
1395 .unwrap_or_default()
1396 .as_millis() as i64;
1397
1398 let start_ms = span
1399 .get("start_ms")
1400 .and_then(|v| v.as_int())
1401 .unwrap_or(end_ms);
1402 let duration_ms = end_ms - start_ms;
1403 let name = span.get("name").map(|v| v.display()).unwrap_or_default();
1404 let trace_id = span
1405 .get("trace_id")
1406 .map(|v| v.display())
1407 .unwrap_or_default();
1408 let span_id = span.get("span_id").map(|v| v.display()).unwrap_or_default();
1409
1410 VM_TRACE_STACK.with(|stack| {
1411 stack.borrow_mut().pop();
1412 });
1413
1414 let level_num = 1_u8;
1415 if level_num >= VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
1416 let mut fields = BTreeMap::new();
1417 fields.insert(
1418 "trace_id".to_string(),
1419 VmValue::String(Rc::from(trace_id.as_str())),
1420 );
1421 fields.insert(
1422 "span_id".to_string(),
1423 VmValue::String(Rc::from(span_id.as_str())),
1424 );
1425 fields.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1426 fields.insert("duration_ms".to_string(), VmValue::Int(duration_ms));
1427 let line = vm_build_log_line("info", "span_end", Some(&fields));
1428 out.push_str(&line);
1429 }
1430
1431 Ok(VmValue::Nil)
1432 });
1433
1434 vm.register_builtin("trace_id", |_args, _out| {
1435 let id = VM_TRACE_STACK.with(|stack| stack.borrow().last().map(|t| t.trace_id.clone()));
1436 match id {
1437 Some(trace_id) => Ok(VmValue::String(Rc::from(trace_id.as_str()))),
1438 None => Ok(VmValue::Nil),
1439 }
1440 });
1441
1442 vm.register_builtin("llm_info", |_args, _out| {
1447 let provider = std::env::var("HARN_LLM_PROVIDER").unwrap_or_default();
1448 let model = std::env::var("HARN_LLM_MODEL").unwrap_or_default();
1449 let api_key_set = std::env::var("HARN_API_KEY")
1450 .or_else(|_| std::env::var("OPENROUTER_API_KEY"))
1451 .or_else(|_| std::env::var("ANTHROPIC_API_KEY"))
1452 .is_ok();
1453 let mut info = BTreeMap::new();
1454 info.insert(
1455 "provider".to_string(),
1456 VmValue::String(Rc::from(provider.as_str())),
1457 );
1458 info.insert(
1459 "model".to_string(),
1460 VmValue::String(Rc::from(model.as_str())),
1461 );
1462 info.insert("api_key_set".to_string(), VmValue::Bool(api_key_set));
1463 Ok(VmValue::Dict(Rc::new(info)))
1464 });
1465
1466 vm.register_builtin("llm_usage", |_args, _out| {
1467 let (total_input, total_output, total_duration, call_count) =
1468 crate::llm::peek_trace_summary();
1469 let mut usage = BTreeMap::new();
1470 usage.insert("input_tokens".to_string(), VmValue::Int(total_input));
1471 usage.insert("output_tokens".to_string(), VmValue::Int(total_output));
1472 usage.insert(
1473 "total_duration_ms".to_string(),
1474 VmValue::Int(total_duration),
1475 );
1476 usage.insert("call_count".to_string(), VmValue::Int(call_count));
1477 Ok(VmValue::Dict(Rc::new(usage)))
1478 });
1479
1480 vm.register_builtin("timer_start", |args, _out| {
1485 let name = args
1486 .first()
1487 .map(|a| a.display())
1488 .unwrap_or_else(|| "default".to_string());
1489 let now_ms = std::time::SystemTime::now()
1490 .duration_since(std::time::UNIX_EPOCH)
1491 .unwrap_or_default()
1492 .as_millis() as i64;
1493 let mut timer = BTreeMap::new();
1494 timer.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1495 timer.insert("start_ms".to_string(), VmValue::Int(now_ms));
1496 Ok(VmValue::Dict(Rc::new(timer)))
1497 });
1498
1499 vm.register_builtin("timer_end", |args, out| {
1500 let timer = match args.first() {
1501 Some(VmValue::Dict(d)) => d,
1502 _ => {
1503 return Err(VmError::Thrown(VmValue::String(Rc::from(
1504 "timer_end: argument must be a timer dict from timer_start",
1505 ))));
1506 }
1507 };
1508 let now_ms = std::time::SystemTime::now()
1509 .duration_since(std::time::UNIX_EPOCH)
1510 .unwrap_or_default()
1511 .as_millis() as i64;
1512 let start_ms = timer
1513 .get("start_ms")
1514 .and_then(|v| v.as_int())
1515 .unwrap_or(now_ms);
1516 let elapsed = now_ms - start_ms;
1517 let name = timer.get("name").map(|v| v.display()).unwrap_or_default();
1518 out.push_str(&format!("[timer] {name}: {elapsed}ms\n"));
1519 Ok(VmValue::Int(elapsed))
1520 });
1521
1522 vm.register_builtin("elapsed", |_args, _out| {
1523 static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
1526 let start = START.get_or_init(std::time::Instant::now);
1527 Ok(VmValue::Int(start.elapsed().as_millis() as i64))
1528 });
1529
1530 vm.register_builtin("log_json", |args, out| {
1531 let key = args.first().map(|a| a.display()).unwrap_or_default();
1532 let value = args.get(1).cloned().unwrap_or(VmValue::Nil);
1533 let json_val = vm_value_to_json_fragment(&value);
1534 let ts = vm_format_timestamp_utc();
1535 out.push_str(&format!(
1536 "{{\"ts\":{},\"key\":{},\"value\":{}}}\n",
1537 vm_escape_json_str_quoted(&ts),
1538 vm_escape_json_str_quoted(&key),
1539 json_val,
1540 ));
1541 Ok(VmValue::Nil)
1542 });
1543
1544 vm.register_builtin("tool_registry", |_args, _out| {
1549 let mut registry = BTreeMap::new();
1550 registry.insert(
1551 "_type".to_string(),
1552 VmValue::String(Rc::from("tool_registry")),
1553 );
1554 registry.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
1555 Ok(VmValue::Dict(Rc::new(registry)))
1556 });
1557
1558 vm.register_builtin("tool_add", |args, _out| {
1559 if args.len() < 4 {
1560 return Err(VmError::Thrown(VmValue::String(Rc::from(
1561 "tool_add: requires registry, name, description, and handler",
1562 ))));
1563 }
1564
1565 let registry = match &args[0] {
1566 VmValue::Dict(map) => (**map).clone(),
1567 _ => {
1568 return Err(VmError::Thrown(VmValue::String(Rc::from(
1569 "tool_add: first argument must be a tool registry",
1570 ))));
1571 }
1572 };
1573
1574 match registry.get("_type") {
1575 Some(VmValue::String(t)) if &**t == "tool_registry" => {}
1576 _ => {
1577 return Err(VmError::Thrown(VmValue::String(Rc::from(
1578 "tool_add: first argument must be a tool registry",
1579 ))));
1580 }
1581 }
1582
1583 let name = args[1].display();
1584 let description = args[2].display();
1585 let handler = args[3].clone();
1586 let parameters = if args.len() > 4 {
1587 args[4].clone()
1588 } else {
1589 VmValue::Dict(Rc::new(BTreeMap::new()))
1590 };
1591
1592 let mut tool_entry = BTreeMap::new();
1593 tool_entry.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1594 tool_entry.insert(
1595 "description".to_string(),
1596 VmValue::String(Rc::from(description.as_str())),
1597 );
1598 tool_entry.insert("handler".to_string(), handler);
1599 tool_entry.insert("parameters".to_string(), parameters);
1600
1601 let mut tools: Vec<VmValue> = match registry.get("tools") {
1602 Some(VmValue::List(list)) => list
1603 .iter()
1604 .filter(|t| {
1605 if let VmValue::Dict(e) = t {
1606 e.get("name").map(|v| v.display()).as_deref() != Some(name.as_str())
1607 } else {
1608 true
1609 }
1610 })
1611 .cloned()
1612 .collect(),
1613 _ => Vec::new(),
1614 };
1615 tools.push(VmValue::Dict(Rc::new(tool_entry)));
1616
1617 let mut new_registry = registry;
1618 new_registry.insert("tools".to_string(), VmValue::List(Rc::new(tools)));
1619 Ok(VmValue::Dict(Rc::new(new_registry)))
1620 });
1621
1622 vm.register_builtin("tool_list", |args, _out| {
1623 let registry = match args.first() {
1624 Some(VmValue::Dict(map)) => map,
1625 _ => {
1626 return Err(VmError::Thrown(VmValue::String(Rc::from(
1627 "tool_list: requires a tool registry",
1628 ))));
1629 }
1630 };
1631 vm_validate_registry("tool_list", registry)?;
1632
1633 let tools = vm_get_tools(registry);
1634 let mut result = Vec::new();
1635 for tool in tools {
1636 if let VmValue::Dict(entry) = tool {
1637 let mut desc = BTreeMap::new();
1638 if let Some(name) = entry.get("name") {
1639 desc.insert("name".to_string(), name.clone());
1640 }
1641 if let Some(description) = entry.get("description") {
1642 desc.insert("description".to_string(), description.clone());
1643 }
1644 if let Some(parameters) = entry.get("parameters") {
1645 desc.insert("parameters".to_string(), parameters.clone());
1646 }
1647 result.push(VmValue::Dict(Rc::new(desc)));
1648 }
1649 }
1650 Ok(VmValue::List(Rc::new(result)))
1651 });
1652
1653 vm.register_builtin("tool_find", |args, _out| {
1654 if args.len() < 2 {
1655 return Err(VmError::Thrown(VmValue::String(Rc::from(
1656 "tool_find: requires registry and name",
1657 ))));
1658 }
1659
1660 let registry = match &args[0] {
1661 VmValue::Dict(map) => map,
1662 _ => {
1663 return Err(VmError::Thrown(VmValue::String(Rc::from(
1664 "tool_find: first argument must be a tool registry",
1665 ))));
1666 }
1667 };
1668 vm_validate_registry("tool_find", registry)?;
1669
1670 let target_name = args[1].display();
1671 let tools = vm_get_tools(registry);
1672
1673 for tool in tools {
1674 if let VmValue::Dict(entry) = tool {
1675 if let Some(VmValue::String(name)) = entry.get("name") {
1676 if &**name == target_name.as_str() {
1677 return Ok(tool.clone());
1678 }
1679 }
1680 }
1681 }
1682 Ok(VmValue::Nil)
1683 });
1684
1685 vm.register_builtin("tool_describe", |args, _out| {
1686 let registry = match args.first() {
1687 Some(VmValue::Dict(map)) => map,
1688 _ => {
1689 return Err(VmError::Thrown(VmValue::String(Rc::from(
1690 "tool_describe: requires a tool registry",
1691 ))));
1692 }
1693 };
1694 vm_validate_registry("tool_describe", registry)?;
1695
1696 let tools = vm_get_tools(registry);
1697
1698 if tools.is_empty() {
1699 return Ok(VmValue::String(Rc::from("Available tools:\n(none)")));
1700 }
1701
1702 let mut tool_infos: Vec<(String, String, String)> = Vec::new();
1703 for tool in tools {
1704 if let VmValue::Dict(entry) = tool {
1705 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1706 let description = entry
1707 .get("description")
1708 .map(|v| v.display())
1709 .unwrap_or_default();
1710 let params_str = vm_format_parameters(entry.get("parameters"));
1711 tool_infos.push((name, params_str, description));
1712 }
1713 }
1714
1715 tool_infos.sort_by(|a, b| a.0.cmp(&b.0));
1716
1717 let mut lines = vec!["Available tools:".to_string()];
1718 for (name, params, desc) in &tool_infos {
1719 lines.push(format!("- {name}({params}): {desc}"));
1720 }
1721
1722 Ok(VmValue::String(Rc::from(lines.join("\n").as_str())))
1723 });
1724
1725 vm.register_builtin("tool_remove", |args, _out| {
1726 if args.len() < 2 {
1727 return Err(VmError::Thrown(VmValue::String(Rc::from(
1728 "tool_remove: requires registry and name",
1729 ))));
1730 }
1731
1732 let registry = match &args[0] {
1733 VmValue::Dict(map) => (**map).clone(),
1734 _ => {
1735 return Err(VmError::Thrown(VmValue::String(Rc::from(
1736 "tool_remove: first argument must be a tool registry",
1737 ))));
1738 }
1739 };
1740 vm_validate_registry("tool_remove", ®istry)?;
1741
1742 let target_name = args[1].display();
1743
1744 let tools = match registry.get("tools") {
1745 Some(VmValue::List(list)) => (**list).clone(),
1746 _ => Vec::new(),
1747 };
1748
1749 let filtered: Vec<VmValue> = tools
1750 .into_iter()
1751 .filter(|tool| {
1752 if let VmValue::Dict(entry) = tool {
1753 if let Some(VmValue::String(name)) = entry.get("name") {
1754 return &**name != target_name.as_str();
1755 }
1756 }
1757 true
1758 })
1759 .collect();
1760
1761 let mut new_registry = registry;
1762 new_registry.insert("tools".to_string(), VmValue::List(Rc::new(filtered)));
1763 Ok(VmValue::Dict(Rc::new(new_registry)))
1764 });
1765
1766 vm.register_builtin("tool_count", |args, _out| {
1767 let registry = match args.first() {
1768 Some(VmValue::Dict(map)) => map,
1769 _ => {
1770 return Err(VmError::Thrown(VmValue::String(Rc::from(
1771 "tool_count: requires a tool registry",
1772 ))));
1773 }
1774 };
1775 vm_validate_registry("tool_count", registry)?;
1776 let count = vm_get_tools(registry).len();
1777 Ok(VmValue::Int(count as i64))
1778 });
1779
1780 vm.register_builtin("tool_schema", |args, _out| {
1781 let registry = match args.first() {
1782 Some(VmValue::Dict(map)) => {
1783 vm_validate_registry("tool_schema", map)?;
1784 map
1785 }
1786 _ => {
1787 return Err(VmError::Thrown(VmValue::String(Rc::from(
1788 "tool_schema: requires a tool registry",
1789 ))));
1790 }
1791 };
1792
1793 let components = args.get(1).and_then(|v| v.as_dict()).cloned();
1794
1795 let tools = match registry.get("tools") {
1796 Some(VmValue::List(list)) => list,
1797 _ => return Ok(VmValue::Dict(Rc::new(vm_build_empty_schema()))),
1798 };
1799
1800 let mut tool_schemas = Vec::new();
1801 for tool in tools.iter() {
1802 if let VmValue::Dict(entry) = tool {
1803 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1804 let description = entry
1805 .get("description")
1806 .map(|v| v.display())
1807 .unwrap_or_default();
1808
1809 let input_schema =
1810 vm_build_input_schema(entry.get("parameters"), components.as_ref());
1811
1812 let mut tool_def = BTreeMap::new();
1813 tool_def.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1814 tool_def.insert(
1815 "description".to_string(),
1816 VmValue::String(Rc::from(description.as_str())),
1817 );
1818 tool_def.insert("inputSchema".to_string(), input_schema);
1819 tool_schemas.push(VmValue::Dict(Rc::new(tool_def)));
1820 }
1821 }
1822
1823 let mut schema = BTreeMap::new();
1824 schema.insert(
1825 "schema_version".to_string(),
1826 VmValue::String(Rc::from("harn-tools/1.0")),
1827 );
1828
1829 if let Some(comps) = &components {
1830 let mut comp_wrapper = BTreeMap::new();
1831 comp_wrapper.insert("schemas".to_string(), VmValue::Dict(Rc::new(comps.clone())));
1832 schema.insert(
1833 "components".to_string(),
1834 VmValue::Dict(Rc::new(comp_wrapper)),
1835 );
1836 }
1837
1838 schema.insert("tools".to_string(), VmValue::List(Rc::new(tool_schemas)));
1839 Ok(VmValue::Dict(Rc::new(schema)))
1840 });
1841
1842 vm.register_builtin("tool_parse_call", |args, _out| {
1843 let text = args.first().map(|a| a.display()).unwrap_or_default();
1844
1845 let mut results = Vec::new();
1846 let mut search_from = 0;
1847
1848 while let Some(start) = text[search_from..].find("<tool_call>") {
1849 let abs_start = search_from + start + "<tool_call>".len();
1850 if let Some(end) = text[abs_start..].find("</tool_call>") {
1851 let json_str = text[abs_start..abs_start + end].trim();
1852 if let Ok(jv) = serde_json::from_str::<serde_json::Value>(json_str) {
1853 results.push(json_to_vm_value(&jv));
1854 }
1855 search_from = abs_start + end + "</tool_call>".len();
1856 } else {
1857 break;
1858 }
1859 }
1860
1861 Ok(VmValue::List(Rc::new(results)))
1862 });
1863
1864 vm.register_builtin("tool_format_result", |args, _out| {
1865 if args.len() < 2 {
1866 return Err(VmError::Thrown(VmValue::String(Rc::from(
1867 "tool_format_result: requires name and result",
1868 ))));
1869 }
1870 let name = args[0].display();
1871 let result = args[1].display();
1872
1873 let json_name = vm_escape_json_str(&name);
1874 let json_result = vm_escape_json_str(&result);
1875 Ok(VmValue::String(Rc::from(
1876 format!(
1877 "<tool_result>{{\"name\": \"{json_name}\", \"result\": \"{json_result}\"}}</tool_result>"
1878 )
1879 .as_str(),
1880 )))
1881 });
1882
1883 vm.register_builtin("tool_prompt", |args, _out| {
1884 let registry = match args.first() {
1885 Some(VmValue::Dict(map)) => {
1886 vm_validate_registry("tool_prompt", map)?;
1887 map
1888 }
1889 _ => {
1890 return Err(VmError::Thrown(VmValue::String(Rc::from(
1891 "tool_prompt: requires a tool registry",
1892 ))));
1893 }
1894 };
1895
1896 let tools = match registry.get("tools") {
1897 Some(VmValue::List(list)) => list,
1898 _ => {
1899 return Ok(VmValue::String(Rc::from("No tools are available.")));
1900 }
1901 };
1902
1903 if tools.is_empty() {
1904 return Ok(VmValue::String(Rc::from("No tools are available.")));
1905 }
1906
1907 let mut prompt = String::from("# Available Tools\n\n");
1908 prompt.push_str("You have access to the following tools. To use a tool, output a tool call in this exact format:\n\n");
1909 prompt.push_str("<tool_call>{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}</tool_call>\n\n");
1910 prompt.push_str("You may make multiple tool calls in a single response. Wait for tool results before proceeding.\n\n");
1911 prompt.push_str("## Tools\n\n");
1912
1913 let mut tool_infos: Vec<(&BTreeMap<String, VmValue>, String)> = Vec::new();
1914 for tool in tools.iter() {
1915 if let VmValue::Dict(entry) = tool {
1916 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1917 tool_infos.push((entry, name));
1918 }
1919 }
1920 tool_infos.sort_by(|a, b| a.1.cmp(&b.1));
1921
1922 for (entry, name) in &tool_infos {
1923 let description = entry
1924 .get("description")
1925 .map(|v| v.display())
1926 .unwrap_or_default();
1927 let params_str = vm_format_parameters(entry.get("parameters"));
1928
1929 prompt.push_str(&format!("### {name}\n"));
1930 prompt.push_str(&format!("{description}\n"));
1931 if !params_str.is_empty() {
1932 prompt.push_str(&format!("Parameters: {params_str}\n"));
1933 }
1934 prompt.push('\n');
1935 }
1936
1937 Ok(VmValue::String(Rc::from(prompt.trim_end())))
1938 });
1939
1940 vm.register_builtin("channel", |args, _out| {
1945 let name = args
1946 .first()
1947 .map(|a| a.display())
1948 .unwrap_or_else(|| "default".to_string());
1949 let capacity = args.get(1).and_then(|a| a.as_int()).unwrap_or(256) as usize;
1950 let capacity = capacity.max(1);
1951 let (tx, rx) = tokio::sync::mpsc::channel(capacity);
1952 #[allow(clippy::arc_with_non_send_sync)]
1953 Ok(VmValue::Channel(VmChannelHandle {
1954 name,
1955 sender: Arc::new(tx),
1956 receiver: Arc::new(tokio::sync::Mutex::new(rx)),
1957 closed: Arc::new(AtomicBool::new(false)),
1958 }))
1959 });
1960
1961 vm.register_builtin("close_channel", |args, _out| {
1962 if args.is_empty() {
1963 return Err(VmError::Thrown(VmValue::String(Rc::from(
1964 "close_channel: requires a channel",
1965 ))));
1966 }
1967 if let VmValue::Channel(ch) = &args[0] {
1968 ch.closed.store(true, Ordering::SeqCst);
1969 Ok(VmValue::Nil)
1970 } else {
1971 Err(VmError::Thrown(VmValue::String(Rc::from(
1972 "close_channel: first argument must be a channel",
1973 ))))
1974 }
1975 });
1976
1977 vm.register_builtin("try_receive", |args, _out| {
1978 if args.is_empty() {
1979 return Err(VmError::Thrown(VmValue::String(Rc::from(
1980 "try_receive: requires a channel",
1981 ))));
1982 }
1983 if let VmValue::Channel(ch) = &args[0] {
1984 match ch.receiver.try_lock() {
1985 Ok(mut rx) => match rx.try_recv() {
1986 Ok(val) => Ok(val),
1987 Err(_) => Ok(VmValue::Nil),
1988 },
1989 Err(_) => Ok(VmValue::Nil),
1990 }
1991 } else {
1992 Err(VmError::Thrown(VmValue::String(Rc::from(
1993 "try_receive: first argument must be a channel",
1994 ))))
1995 }
1996 });
1997
1998 vm.register_builtin("atomic", |args, _out| {
2003 let initial = match args.first() {
2004 Some(VmValue::Int(n)) => *n,
2005 Some(VmValue::Float(f)) => *f as i64,
2006 Some(VmValue::Bool(b)) => {
2007 if *b {
2008 1
2009 } else {
2010 0
2011 }
2012 }
2013 _ => 0,
2014 };
2015 Ok(VmValue::Atomic(VmAtomicHandle {
2016 value: Arc::new(AtomicI64::new(initial)),
2017 }))
2018 });
2019
2020 vm.register_builtin("atomic_get", |args, _out| {
2021 if let Some(VmValue::Atomic(a)) = args.first() {
2022 Ok(VmValue::Int(a.value.load(Ordering::SeqCst)))
2023 } else {
2024 Ok(VmValue::Nil)
2025 }
2026 });
2027
2028 vm.register_builtin("atomic_set", |args, _out| {
2029 if args.len() >= 2 {
2030 if let (VmValue::Atomic(a), Some(val)) = (&args[0], args[1].as_int()) {
2031 let old = a.value.swap(val, Ordering::SeqCst);
2032 return Ok(VmValue::Int(old));
2033 }
2034 }
2035 Ok(VmValue::Nil)
2036 });
2037
2038 vm.register_builtin("atomic_add", |args, _out| {
2039 if args.len() >= 2 {
2040 if let (VmValue::Atomic(a), Some(delta)) = (&args[0], args[1].as_int()) {
2041 let prev = a.value.fetch_add(delta, Ordering::SeqCst);
2042 return Ok(VmValue::Int(prev));
2043 }
2044 }
2045 Ok(VmValue::Nil)
2046 });
2047
2048 vm.register_builtin("atomic_cas", |args, _out| {
2049 if args.len() >= 3 {
2050 if let (VmValue::Atomic(a), Some(expected), Some(new_val)) =
2051 (&args[0], args[1].as_int(), args[2].as_int())
2052 {
2053 let result =
2054 a.value
2055 .compare_exchange(expected, new_val, Ordering::SeqCst, Ordering::SeqCst);
2056 return Ok(VmValue::Bool(result.is_ok()));
2057 }
2058 }
2059 Ok(VmValue::Bool(false))
2060 });
2061
2062 vm.register_async_builtin("sleep", |args| async move {
2068 let ms = match args.first() {
2069 Some(VmValue::Duration(ms)) => *ms,
2070 Some(VmValue::Int(n)) => *n as u64,
2071 _ => 0,
2072 };
2073 if ms > 0 {
2074 tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
2075 }
2076 Ok(VmValue::Nil)
2077 });
2078
2079 vm.register_async_builtin("send", |args| async move {
2081 if args.len() < 2 {
2082 return Err(VmError::Thrown(VmValue::String(Rc::from(
2083 "send: requires channel and value",
2084 ))));
2085 }
2086 if let VmValue::Channel(ch) = &args[0] {
2087 if ch.closed.load(Ordering::SeqCst) {
2088 return Ok(VmValue::Bool(false));
2089 }
2090 let val = args[1].clone();
2091 match ch.sender.send(val).await {
2092 Ok(()) => Ok(VmValue::Bool(true)),
2093 Err(_) => Ok(VmValue::Bool(false)),
2094 }
2095 } else {
2096 Err(VmError::Thrown(VmValue::String(Rc::from(
2097 "send: first argument must be a channel",
2098 ))))
2099 }
2100 });
2101
2102 vm.register_async_builtin("receive", |args| async move {
2104 if args.is_empty() {
2105 return Err(VmError::Thrown(VmValue::String(Rc::from(
2106 "receive: requires a channel",
2107 ))));
2108 }
2109 if let VmValue::Channel(ch) = &args[0] {
2110 if ch.closed.load(Ordering::SeqCst) {
2111 let mut rx = ch.receiver.lock().await;
2112 return match rx.try_recv() {
2113 Ok(val) => Ok(val),
2114 Err(_) => Ok(VmValue::Nil),
2115 };
2116 }
2117 let mut rx = ch.receiver.lock().await;
2118 match rx.recv().await {
2119 Some(val) => Ok(val),
2120 None => Ok(VmValue::Nil),
2121 }
2122 } else {
2123 Err(VmError::Thrown(VmValue::String(Rc::from(
2124 "receive: first argument must be a channel",
2125 ))))
2126 }
2127 });
2128
2129 vm.register_async_builtin("select", |args| async move {
2131 if args.is_empty() {
2132 return Err(VmError::Thrown(VmValue::String(Rc::from(
2133 "select: requires at least one channel",
2134 ))));
2135 }
2136 for arg in &args {
2137 if !matches!(arg, VmValue::Channel(_)) {
2138 return Err(VmError::Thrown(VmValue::String(Rc::from(
2139 "select: all arguments must be channels",
2140 ))));
2141 }
2142 }
2143 loop {
2144 let (found, all_closed) = try_poll_channels(&args);
2145 if let Some((i, val, name)) = found {
2146 return Ok(select_result(i, val, &name));
2147 }
2148 if all_closed {
2149 return Ok(select_none());
2150 }
2151 tokio::task::yield_now().await;
2152 }
2153 });
2154
2155 vm.register_async_builtin("__select_timeout", |args| async move {
2157 if args.len() < 2 {
2158 return Err(VmError::Thrown(VmValue::String(Rc::from(
2159 "__select_timeout: requires channel list and timeout",
2160 ))));
2161 }
2162 let channels = match &args[0] {
2163 VmValue::List(items) => (**items).clone(),
2164 _ => {
2165 return Err(VmError::Thrown(VmValue::String(Rc::from(
2166 "__select_timeout: first argument must be a list of channels",
2167 ))));
2168 }
2169 };
2170 let timeout_ms = match &args[1] {
2171 VmValue::Int(n) => (*n).max(0) as u64,
2172 VmValue::Duration(ms) => *ms,
2173 _ => 5000,
2174 };
2175 let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_millis(timeout_ms);
2176 loop {
2177 let (found, all_closed) = try_poll_channels(&channels);
2178 if let Some((i, val, name)) = found {
2179 return Ok(select_result(i, val, &name));
2180 }
2181 if all_closed || tokio::time::Instant::now() >= deadline {
2182 return Ok(select_none());
2183 }
2184 tokio::task::yield_now().await;
2185 }
2186 });
2187
2188 vm.register_async_builtin("__select_try", |args| async move {
2190 if args.is_empty() {
2191 return Err(VmError::Thrown(VmValue::String(Rc::from(
2192 "__select_try: requires channel list",
2193 ))));
2194 }
2195 let channels = match &args[0] {
2196 VmValue::List(items) => (**items).clone(),
2197 _ => {
2198 return Err(VmError::Thrown(VmValue::String(Rc::from(
2199 "__select_try: first argument must be a list of channels",
2200 ))));
2201 }
2202 };
2203 let (found, _) = try_poll_channels(&channels);
2204 if let Some((i, val, name)) = found {
2205 Ok(select_result(i, val, &name))
2206 } else {
2207 Ok(select_none())
2208 }
2209 });
2210
2211 vm.register_async_builtin("__select_list", |args| async move {
2213 if args.is_empty() {
2214 return Err(VmError::Thrown(VmValue::String(Rc::from(
2215 "__select_list: requires channel list",
2216 ))));
2217 }
2218 let channels = match &args[0] {
2219 VmValue::List(items) => (**items).clone(),
2220 _ => {
2221 return Err(VmError::Thrown(VmValue::String(Rc::from(
2222 "__select_list: first argument must be a list of channels",
2223 ))));
2224 }
2225 };
2226 loop {
2227 let (found, all_closed) = try_poll_channels(&channels);
2228 if let Some((i, val, name)) = found {
2229 return Ok(select_result(i, val, &name));
2230 }
2231 if all_closed {
2232 return Ok(select_none());
2233 }
2234 tokio::task::yield_now().await;
2235 }
2236 });
2237
2238 vm.register_builtin("json_validate", |args, _out| {
2243 if args.len() < 2 {
2244 return Err(VmError::Thrown(VmValue::String(Rc::from(
2245 "json_validate requires 2 arguments: data and schema",
2246 ))));
2247 }
2248 let data = &args[0];
2249 let schema = &args[1];
2250 let schema_dict = match schema.as_dict() {
2251 Some(d) => d,
2252 None => {
2253 return Err(VmError::Thrown(VmValue::String(Rc::from(
2254 "json_validate: schema must be a dict",
2255 ))));
2256 }
2257 };
2258 let mut errors = Vec::new();
2259 validate_value(data, schema_dict, "", &mut errors);
2260 if errors.is_empty() {
2261 Ok(VmValue::Bool(true))
2262 } else {
2263 Err(VmError::Thrown(VmValue::String(Rc::from(
2264 errors.join("; "),
2265 ))))
2266 }
2267 });
2268
2269 vm.register_builtin("json_extract", |args, _out| {
2270 if args.is_empty() {
2271 return Err(VmError::Thrown(VmValue::String(Rc::from(
2272 "json_extract requires at least 1 argument: text",
2273 ))));
2274 }
2275 let text = args[0].display();
2276 let key = args.get(1).map(|a| a.display());
2277
2278 let json_str = extract_json_from_text(&text);
2280 let parsed = match serde_json::from_str::<serde_json::Value>(&json_str) {
2281 Ok(jv) => json_to_vm_value(&jv),
2282 Err(e) => {
2283 return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2284 "json_extract: failed to parse JSON: {e}"
2285 )))));
2286 }
2287 };
2288
2289 match key {
2290 Some(k) => match &parsed {
2291 VmValue::Dict(map) => match map.get(&k) {
2292 Some(val) => Ok(val.clone()),
2293 None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2294 "json_extract: key '{}' not found",
2295 k
2296 ))))),
2297 },
2298 _ => Err(VmError::Thrown(VmValue::String(Rc::from(
2299 "json_extract: parsed value is not a dict, cannot extract key",
2300 )))),
2301 },
2302 None => Ok(parsed),
2303 }
2304 });
2305
2306 vm.register_builtin("__assert_dict", |args, _out| {
2315 let val = args.first().cloned().unwrap_or(VmValue::Nil);
2316 if matches!(val, VmValue::Dict(_)) {
2317 Ok(VmValue::Nil)
2318 } else {
2319 Err(VmError::TypeError(format!(
2320 "cannot destructure {} with {{...}} pattern — expected dict",
2321 val.type_name()
2322 )))
2323 }
2324 });
2325
2326 vm.register_builtin("__assert_list", |args, _out| {
2327 let val = args.first().cloned().unwrap_or(VmValue::Nil);
2328 if matches!(val, VmValue::List(_)) {
2329 Ok(VmValue::Nil)
2330 } else {
2331 Err(VmError::TypeError(format!(
2332 "cannot destructure {} with [...] pattern — expected list",
2333 val.type_name()
2334 )))
2335 }
2336 });
2337
2338 vm.register_builtin("__dict_rest", |args, _out| {
2339 let dict = args.first().cloned().unwrap_or(VmValue::Nil);
2341 let keys_list = args.get(1).cloned().unwrap_or(VmValue::Nil);
2342 if let VmValue::Dict(map) = dict {
2343 let exclude: std::collections::HashSet<String> = match keys_list {
2344 VmValue::List(items) => items
2345 .iter()
2346 .filter_map(|v| {
2347 if let VmValue::String(s) = v {
2348 Some(s.to_string())
2349 } else {
2350 None
2351 }
2352 })
2353 .collect(),
2354 _ => std::collections::HashSet::new(),
2355 };
2356 let rest: BTreeMap<String, VmValue> = map
2357 .iter()
2358 .filter(|(k, _)| !exclude.contains(k.as_str()))
2359 .map(|(k, v)| (k.clone(), v.clone()))
2360 .collect();
2361 Ok(VmValue::Dict(Rc::new(rest)))
2362 } else {
2363 Ok(VmValue::Nil)
2364 }
2365 });
2366
2367 register_http_builtins(vm);
2368 register_llm_builtins(vm);
2369 register_mcp_builtins(vm);
2370}
2371
2372pub(crate) fn escape_json_string_vm(s: &str) -> String {
2377 let mut out = String::with_capacity(s.len() + 2);
2378 out.push('"');
2379 for ch in s.chars() {
2380 match ch {
2381 '"' => out.push_str("\\\""),
2382 '\\' => out.push_str("\\\\"),
2383 '\n' => out.push_str("\\n"),
2384 '\r' => out.push_str("\\r"),
2385 '\t' => out.push_str("\\t"),
2386 c if c.is_control() => {
2387 out.push_str(&format!("\\u{:04x}", c as u32));
2388 }
2389 c => out.push(c),
2390 }
2391 }
2392 out.push('"');
2393 out
2394}
2395
2396pub(crate) fn vm_value_to_json(val: &VmValue) -> String {
2397 match val {
2398 VmValue::String(s) => escape_json_string_vm(s),
2399 VmValue::Int(n) => n.to_string(),
2400 VmValue::Float(n) => n.to_string(),
2401 VmValue::Bool(b) => b.to_string(),
2402 VmValue::Nil => "null".to_string(),
2403 VmValue::List(items) => {
2404 let inner: Vec<String> = items.iter().map(vm_value_to_json).collect();
2405 format!("[{}]", inner.join(","))
2406 }
2407 VmValue::Dict(map) => {
2408 let inner: Vec<String> = map
2409 .iter()
2410 .map(|(k, v)| format!("{}:{}", escape_json_string_vm(k), vm_value_to_json(v)))
2411 .collect();
2412 format!("{{{}}}", inner.join(","))
2413 }
2414 VmValue::Set(items) => {
2415 let inner: Vec<String> = items.iter().map(vm_value_to_json).collect();
2416 format!("[{}]", inner.join(","))
2417 }
2418 _ => "null".to_string(),
2419 }
2420}
2421
2422pub(crate) fn json_to_vm_value(jv: &serde_json::Value) -> VmValue {
2423 match jv {
2424 serde_json::Value::Null => VmValue::Nil,
2425 serde_json::Value::Bool(b) => VmValue::Bool(*b),
2426 serde_json::Value::Number(n) => {
2427 if let Some(i) = n.as_i64() {
2428 VmValue::Int(i)
2429 } else {
2430 VmValue::Float(n.as_f64().unwrap_or(0.0))
2431 }
2432 }
2433 serde_json::Value::String(s) => VmValue::String(Rc::from(s.as_str())),
2434 serde_json::Value::Array(arr) => {
2435 VmValue::List(Rc::new(arr.iter().map(json_to_vm_value).collect()))
2436 }
2437 serde_json::Value::Object(map) => {
2438 let mut m = BTreeMap::new();
2439 for (k, v) in map {
2440 m.insert(k.clone(), json_to_vm_value(v));
2441 }
2442 VmValue::Dict(Rc::new(m))
2443 }
2444 }
2445}
2446
2447fn validate_value(
2452 value: &VmValue,
2453 schema: &BTreeMap<String, VmValue>,
2454 path: &str,
2455 errors: &mut Vec<String>,
2456) {
2457 if let Some(VmValue::String(expected_type)) = schema.get("type") {
2459 let actual_type = value.type_name();
2460 let type_str: &str = expected_type;
2461 if type_str != "any" && actual_type != type_str {
2462 let location = if path.is_empty() {
2463 "root".to_string()
2464 } else {
2465 path.to_string()
2466 };
2467 errors.push(format!(
2468 "at {}: expected type '{}', got '{}'",
2469 location, type_str, actual_type
2470 ));
2471 return; }
2473 }
2474
2475 if let Some(VmValue::List(required_keys)) = schema.get("required") {
2477 if let VmValue::Dict(map) = value {
2478 for key_val in required_keys.iter() {
2479 let key = key_val.display();
2480 if !map.contains_key(&key) {
2481 let location = if path.is_empty() {
2482 "root".to_string()
2483 } else {
2484 path.to_string()
2485 };
2486 errors.push(format!("at {}: missing required key '{}'", location, key));
2487 }
2488 }
2489 }
2490 }
2491
2492 if let Some(VmValue::Dict(prop_schemas)) = schema.get("properties") {
2494 if let VmValue::Dict(map) = value {
2495 for (key, prop_schema) in prop_schemas.iter() {
2496 if let Some(prop_value) = map.get(key) {
2497 if let Some(prop_schema_dict) = prop_schema.as_dict() {
2498 let child_path = if path.is_empty() {
2499 key.clone()
2500 } else {
2501 format!("{}.{}", path, key)
2502 };
2503 validate_value(prop_value, prop_schema_dict, &child_path, errors);
2504 }
2505 }
2506 }
2507 }
2508 }
2509
2510 if let Some(VmValue::Dict(item_schema)) = schema.get("items") {
2512 if let VmValue::List(items) = value {
2513 for (i, item) in items.iter().enumerate() {
2514 let child_path = if path.is_empty() {
2515 format!("[{}]", i)
2516 } else {
2517 format!("{}[{}]", path, i)
2518 };
2519 validate_value(item, item_schema, &child_path, errors);
2520 }
2521 }
2522 }
2523}
2524
2525fn extract_json_from_text(text: &str) -> String {
2530 let trimmed = text.trim();
2531
2532 if let Some(start) = trimmed.find("```") {
2534 let after_backticks = &trimmed[start + 3..];
2535 let content_start = if let Some(nl) = after_backticks.find('\n') {
2537 nl + 1
2538 } else {
2539 0
2540 };
2541 let content = &after_backticks[content_start..];
2542 if let Some(end) = content.find("```") {
2543 return content[..end].trim().to_string();
2544 }
2545 }
2546
2547 if let Some(obj_start) = trimmed.find('{') {
2550 if let Some(obj_end) = trimmed.rfind('}') {
2551 if obj_end > obj_start {
2552 return trimmed[obj_start..=obj_end].to_string();
2553 }
2554 }
2555 }
2556 if let Some(arr_start) = trimmed.find('[') {
2557 if let Some(arr_end) = trimmed.rfind(']') {
2558 if arr_end > arr_start {
2559 return trimmed[arr_start..=arr_end].to_string();
2560 }
2561 }
2562 }
2563
2564 trimmed.to_string()
2566}
2567
2568fn vm_output_to_value(output: std::process::Output) -> VmValue {
2573 let mut result = BTreeMap::new();
2574 result.insert(
2575 "stdout".to_string(),
2576 VmValue::String(Rc::from(
2577 String::from_utf8_lossy(&output.stdout).to_string().as_str(),
2578 )),
2579 );
2580 result.insert(
2581 "stderr".to_string(),
2582 VmValue::String(Rc::from(
2583 String::from_utf8_lossy(&output.stderr).to_string().as_str(),
2584 )),
2585 );
2586 result.insert(
2587 "status".to_string(),
2588 VmValue::Int(output.status.code().unwrap_or(-1) as i64),
2589 );
2590 result.insert(
2591 "success".to_string(),
2592 VmValue::Bool(output.status.success()),
2593 );
2594 VmValue::Dict(Rc::new(result))
2595}
2596
2597fn vm_civil_from_timestamp(total_secs: u64) -> (i64, i64, i64, i64, i64, i64, i64) {
2602 let days = total_secs / 86400;
2603 let time_of_day = total_secs % 86400;
2604 let hour = (time_of_day / 3600) as i64;
2605 let minute = ((time_of_day % 3600) / 60) as i64;
2606 let second = (time_of_day % 60) as i64;
2607
2608 let z = days as i64 + 719468;
2609 let era = if z >= 0 { z } else { z - 146096 } / 146097;
2610 let doe = (z - era * 146097) as u64;
2611 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2612 let y = yoe as i64 + era * 400;
2613 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2614 let mp = (5 * doy + 2) / 153;
2615 let d = (doy - (153 * mp + 2) / 5 + 1) as i64;
2616 let m = if mp < 10 { mp + 3 } else { mp - 9 } as i64;
2617 let y = if m <= 2 { y + 1 } else { y };
2618 let dow = ((days + 4) % 7) as i64;
2619
2620 (y, m, d, hour, minute, second, dow)
2621}
2622
2623pub(crate) static VM_MIN_LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
2628
2629#[derive(Clone)]
2630pub(crate) struct VmTraceContext {
2631 pub(crate) trace_id: String,
2632 pub(crate) span_id: String,
2633}
2634
2635thread_local! {
2636 pub(crate) static VM_TRACE_STACK: std::cell::RefCell<Vec<VmTraceContext>> = const { std::cell::RefCell::new(Vec::new()) };
2637}
2638
2639fn vm_level_to_u8(level: &str) -> Option<u8> {
2640 match level {
2641 "debug" => Some(0),
2642 "info" => Some(1),
2643 "warn" => Some(2),
2644 "error" => Some(3),
2645 _ => None,
2646 }
2647}
2648
2649fn vm_format_timestamp_utc() -> String {
2650 let now = std::time::SystemTime::now()
2651 .duration_since(std::time::UNIX_EPOCH)
2652 .unwrap_or_default();
2653 let total_secs = now.as_secs();
2654 let millis = now.subsec_millis();
2655
2656 let days = total_secs / 86400;
2657 let time_of_day = total_secs % 86400;
2658 let hour = time_of_day / 3600;
2659 let minute = (time_of_day % 3600) / 60;
2660 let second = time_of_day % 60;
2661
2662 let z = days as i64 + 719468;
2663 let era = if z >= 0 { z } else { z - 146096 } / 146097;
2664 let doe = (z - era * 146097) as u64;
2665 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
2666 let y = yoe as i64 + era * 400;
2667 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2668 let mp = (5 * doy + 2) / 153;
2669 let d = doy - (153 * mp + 2) / 5 + 1;
2670 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2671 let y = if m <= 2 { y + 1 } else { y };
2672
2673 format!("{y:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
2674}
2675
2676pub(crate) fn vm_escape_json_str(s: &str) -> String {
2677 let mut out = String::with_capacity(s.len());
2678 for ch in s.chars() {
2679 match ch {
2680 '"' => out.push_str("\\\""),
2681 '\\' => out.push_str("\\\\"),
2682 '\n' => out.push_str("\\n"),
2683 '\r' => out.push_str("\\r"),
2684 '\t' => out.push_str("\\t"),
2685 c if c.is_control() => {
2686 out.push_str(&format!("\\u{:04x}", c as u32));
2687 }
2688 c => out.push(c),
2689 }
2690 }
2691 out
2692}
2693
2694fn vm_escape_json_str_quoted(s: &str) -> String {
2695 let mut out = String::with_capacity(s.len() + 2);
2696 out.push('"');
2697 out.push_str(&vm_escape_json_str(s));
2698 out.push('"');
2699 out
2700}
2701
2702fn vm_value_to_json_fragment(val: &VmValue) -> String {
2703 match val {
2704 VmValue::String(s) => vm_escape_json_str_quoted(s),
2705 VmValue::Int(n) => n.to_string(),
2706 VmValue::Float(n) => {
2707 if n.is_finite() {
2708 n.to_string()
2709 } else {
2710 "null".to_string()
2711 }
2712 }
2713 VmValue::Bool(b) => b.to_string(),
2714 VmValue::Nil => "null".to_string(),
2715 _ => vm_escape_json_str_quoted(&val.display()),
2716 }
2717}
2718
2719fn vm_build_log_line(level: &str, msg: &str, fields: Option<&BTreeMap<String, VmValue>>) -> String {
2720 let ts = vm_format_timestamp_utc();
2721 let mut parts: Vec<String> = Vec::new();
2722 parts.push(format!("\"ts\":{}", vm_escape_json_str_quoted(&ts)));
2723 parts.push(format!("\"level\":{}", vm_escape_json_str_quoted(level)));
2724 parts.push(format!("\"msg\":{}", vm_escape_json_str_quoted(msg)));
2725
2726 VM_TRACE_STACK.with(|stack| {
2727 if let Some(trace) = stack.borrow().last() {
2728 parts.push(format!(
2729 "\"trace_id\":{}",
2730 vm_escape_json_str_quoted(&trace.trace_id)
2731 ));
2732 parts.push(format!(
2733 "\"span_id\":{}",
2734 vm_escape_json_str_quoted(&trace.span_id)
2735 ));
2736 }
2737 });
2738
2739 if let Some(dict) = fields {
2740 for (k, v) in dict {
2741 parts.push(format!(
2742 "{}:{}",
2743 vm_escape_json_str_quoted(k),
2744 vm_value_to_json_fragment(v)
2745 ));
2746 }
2747 }
2748
2749 format!("{{{}}}\n", parts.join(","))
2750}
2751
2752fn vm_write_log(level: &str, level_num: u8, args: &[VmValue], out: &mut String) {
2753 if level_num < VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
2754 return;
2755 }
2756 let msg = args.first().map(|a| a.display()).unwrap_or_default();
2757 let fields = args.get(1).and_then(|v| {
2758 if let VmValue::Dict(d) = v {
2759 Some(&**d)
2760 } else {
2761 None
2762 }
2763 });
2764 let line = vm_build_log_line(level, &msg, fields);
2765 out.push_str(&line);
2766}
2767
2768fn vm_validate_registry(name: &str, dict: &BTreeMap<String, VmValue>) -> Result<(), VmError> {
2773 match dict.get("_type") {
2774 Some(VmValue::String(t)) if &**t == "tool_registry" => Ok(()),
2775 _ => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2776 "{name}: argument must be a tool registry (created with tool_registry())"
2777 ))))),
2778 }
2779}
2780
2781fn vm_get_tools(dict: &BTreeMap<String, VmValue>) -> &[VmValue] {
2782 match dict.get("tools") {
2783 Some(VmValue::List(list)) => list,
2784 _ => &[],
2785 }
2786}
2787
2788fn vm_format_parameters(params: Option<&VmValue>) -> String {
2789 match params {
2790 Some(VmValue::Dict(map)) if !map.is_empty() => {
2791 let mut pairs: Vec<(String, String)> =
2792 map.iter().map(|(k, v)| (k.clone(), v.display())).collect();
2793 pairs.sort_by(|a, b| a.0.cmp(&b.0));
2794 pairs
2795 .iter()
2796 .map(|(k, v)| format!("{k}: {v}"))
2797 .collect::<Vec<_>>()
2798 .join(", ")
2799 }
2800 _ => String::new(),
2801 }
2802}
2803
2804fn vm_build_empty_schema() -> BTreeMap<String, VmValue> {
2805 let mut schema = BTreeMap::new();
2806 schema.insert(
2807 "schema_version".to_string(),
2808 VmValue::String(Rc::from("harn-tools/1.0")),
2809 );
2810 schema.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
2811 schema
2812}
2813
2814fn vm_build_input_schema(
2815 params: Option<&VmValue>,
2816 components: Option<&BTreeMap<String, VmValue>>,
2817) -> VmValue {
2818 let mut schema = BTreeMap::new();
2819 schema.insert("type".to_string(), VmValue::String(Rc::from("object")));
2820
2821 let params_map = match params {
2822 Some(VmValue::Dict(map)) if !map.is_empty() => map,
2823 _ => {
2824 schema.insert(
2825 "properties".to_string(),
2826 VmValue::Dict(Rc::new(BTreeMap::new())),
2827 );
2828 return VmValue::Dict(Rc::new(schema));
2829 }
2830 };
2831
2832 let mut properties = BTreeMap::new();
2833 let mut required = Vec::new();
2834
2835 for (key, val) in params_map.iter() {
2836 let prop = vm_resolve_param_type(val, components);
2837 properties.insert(key.clone(), prop);
2838 required.push(VmValue::String(Rc::from(key.as_str())));
2839 }
2840
2841 schema.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
2842 if !required.is_empty() {
2843 required.sort_by_key(|a| a.display());
2844 schema.insert("required".to_string(), VmValue::List(Rc::new(required)));
2845 }
2846
2847 VmValue::Dict(Rc::new(schema))
2848}
2849
2850fn vm_resolve_param_type(val: &VmValue, components: Option<&BTreeMap<String, VmValue>>) -> VmValue {
2851 match val {
2852 VmValue::String(type_name) => {
2853 let json_type = vm_harn_type_to_json_schema(type_name);
2854 let mut prop = BTreeMap::new();
2855 prop.insert("type".to_string(), VmValue::String(Rc::from(json_type)));
2856 VmValue::Dict(Rc::new(prop))
2857 }
2858 VmValue::Dict(map) => {
2859 if let Some(VmValue::String(ref_name)) = map.get("$ref") {
2860 if let Some(comps) = components {
2861 if let Some(resolved) = comps.get(&**ref_name) {
2862 return resolved.clone();
2863 }
2864 }
2865 let mut prop = BTreeMap::new();
2866 prop.insert(
2867 "$ref".to_string(),
2868 VmValue::String(Rc::from(
2869 format!("#/components/schemas/{ref_name}").as_str(),
2870 )),
2871 );
2872 VmValue::Dict(Rc::new(prop))
2873 } else {
2874 VmValue::Dict(Rc::new((**map).clone()))
2875 }
2876 }
2877 _ => {
2878 let mut prop = BTreeMap::new();
2879 prop.insert("type".to_string(), VmValue::String(Rc::from("string")));
2880 VmValue::Dict(Rc::new(prop))
2881 }
2882 }
2883}
2884
2885fn vm_harn_type_to_json_schema(harn_type: &str) -> &str {
2886 match harn_type {
2887 "int" => "integer",
2888 "float" => "number",
2889 "bool" | "boolean" => "boolean",
2890 "list" | "array" => "array",
2891 "dict" | "object" => "object",
2892 _ => "string",
2893 }
2894}