1use std::collections::BTreeMap;
2use std::io::BufRead;
3use std::rc::Rc;
4use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, Ordering};
5use std::sync::Arc;
6
7use crate::value::{values_equal, VmAtomicHandle, VmChannelHandle, VmError, VmValue};
8use crate::vm::Vm;
9
10use crate::http::register_http_builtins;
11use crate::llm::register_llm_builtins;
12use crate::mcp::register_mcp_builtins;
13
14pub fn register_vm_stdlib(vm: &mut Vm) {
16 vm.register_builtin("log", |args, out| {
17 let msg = args.first().map(|a| a.display()).unwrap_or_default();
18 out.push_str(&format!("[harn] {msg}\n"));
19 Ok(VmValue::Nil)
20 });
21 vm.register_builtin("print", |args, out| {
22 let msg = args.first().map(|a| a.display()).unwrap_or_default();
23 out.push_str(&msg);
24 Ok(VmValue::Nil)
25 });
26 vm.register_builtin("println", |args, out| {
27 let msg = args.first().map(|a| a.display()).unwrap_or_default();
28 out.push_str(&format!("{msg}\n"));
29 Ok(VmValue::Nil)
30 });
31 vm.register_builtin("type_of", |args, _out| {
32 let val = args.first().unwrap_or(&VmValue::Nil);
33 Ok(VmValue::String(Rc::from(val.type_name())))
34 });
35 vm.register_builtin("to_string", |args, _out| {
36 let val = args.first().unwrap_or(&VmValue::Nil);
37 Ok(VmValue::String(Rc::from(val.display())))
38 });
39 vm.register_builtin("to_int", |args, _out| {
40 let val = args.first().unwrap_or(&VmValue::Nil);
41 match val {
42 VmValue::Int(n) => Ok(VmValue::Int(*n)),
43 VmValue::Float(n) => Ok(VmValue::Int(*n as i64)),
44 VmValue::String(s) => Ok(s.parse::<i64>().map(VmValue::Int).unwrap_or(VmValue::Nil)),
45 _ => Ok(VmValue::Nil),
46 }
47 });
48 vm.register_builtin("to_float", |args, _out| {
49 let val = args.first().unwrap_or(&VmValue::Nil);
50 match val {
51 VmValue::Float(n) => Ok(VmValue::Float(*n)),
52 VmValue::Int(n) => Ok(VmValue::Float(*n as f64)),
53 VmValue::String(s) => Ok(s.parse::<f64>().map(VmValue::Float).unwrap_or(VmValue::Nil)),
54 _ => Ok(VmValue::Nil),
55 }
56 });
57
58 vm.register_builtin("json_stringify", |args, _out| {
59 let val = args.first().unwrap_or(&VmValue::Nil);
60 Ok(VmValue::String(Rc::from(vm_value_to_json(val))))
61 });
62
63 vm.register_builtin("json_parse", |args, _out| {
64 let text = args.first().map(|a| a.display()).unwrap_or_default();
65 match serde_json::from_str::<serde_json::Value>(&text) {
66 Ok(jv) => Ok(json_to_vm_value(&jv)),
67 Err(e) => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
68 "JSON parse error: {e}"
69 ))))),
70 }
71 });
72
73 vm.register_builtin("env", |args, _out| {
74 let name = args.first().map(|a| a.display()).unwrap_or_default();
75 match std::env::var(&name) {
76 Ok(val) => Ok(VmValue::String(Rc::from(val))),
77 Err(_) => Ok(VmValue::Nil),
78 }
79 });
80
81 vm.register_builtin("timestamp", |_args, _out| {
82 use std::time::{SystemTime, UNIX_EPOCH};
83 let secs = SystemTime::now()
84 .duration_since(UNIX_EPOCH)
85 .map(|d| d.as_secs_f64())
86 .unwrap_or(0.0);
87 Ok(VmValue::Float(secs))
88 });
89
90 vm.register_builtin("read_file", |args, _out| {
91 let path = args.first().map(|a| a.display()).unwrap_or_default();
92 match std::fs::read_to_string(&path) {
93 Ok(content) => Ok(VmValue::String(Rc::from(content))),
94 Err(e) => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
95 "Failed to read file {path}: {e}"
96 ))))),
97 }
98 });
99
100 vm.register_builtin("write_file", |args, _out| {
101 if args.len() >= 2 {
102 let path = args[0].display();
103 let content = args[1].display();
104 std::fs::write(&path, &content).map_err(|e| {
105 VmError::Thrown(VmValue::String(Rc::from(format!(
106 "Failed to write file {path}: {e}"
107 ))))
108 })?;
109 }
110 Ok(VmValue::Nil)
111 });
112
113 vm.register_builtin("exit", |args, _out| {
114 let code = args.first().and_then(|a| a.as_int()).unwrap_or(0);
115 std::process::exit(code as i32);
116 });
117
118 vm.register_builtin("regex_match", |args, _out| {
119 if args.len() >= 2 {
120 let pattern = args[0].display();
121 let text = args[1].display();
122 let re = regex::Regex::new(&pattern).map_err(|e| {
123 VmError::Thrown(VmValue::String(Rc::from(format!("Invalid regex: {e}"))))
124 })?;
125 let matches: Vec<VmValue> = re
126 .find_iter(&text)
127 .map(|m| VmValue::String(Rc::from(m.as_str())))
128 .collect();
129 if matches.is_empty() {
130 return Ok(VmValue::Nil);
131 }
132 return Ok(VmValue::List(Rc::new(matches)));
133 }
134 Ok(VmValue::Nil)
135 });
136
137 vm.register_builtin("regex_replace", |args, _out| {
138 if args.len() >= 3 {
139 let pattern = args[0].display();
140 let replacement = args[1].display();
141 let text = args[2].display();
142 let re = regex::Regex::new(&pattern).map_err(|e| {
143 VmError::Thrown(VmValue::String(Rc::from(format!("Invalid regex: {e}"))))
144 })?;
145 return Ok(VmValue::String(Rc::from(
146 re.replace_all(&text, replacement.as_str()).into_owned(),
147 )));
148 }
149 Ok(VmValue::Nil)
150 });
151
152 vm.register_builtin("prompt_user", |args, out| {
153 let msg = args.first().map(|a| a.display()).unwrap_or_default();
154 out.push_str(&msg);
155 let mut input = String::new();
156 if std::io::stdin().lock().read_line(&mut input).is_ok() {
157 Ok(VmValue::String(Rc::from(input.trim_end())))
158 } else {
159 Ok(VmValue::Nil)
160 }
161 });
162
163 vm.register_builtin("abs", |args, _out| {
166 match args.first().unwrap_or(&VmValue::Nil) {
167 VmValue::Int(n) => Ok(VmValue::Int(n.wrapping_abs())),
168 VmValue::Float(n) => Ok(VmValue::Float(n.abs())),
169 _ => Ok(VmValue::Nil),
170 }
171 });
172
173 vm.register_builtin("min", |args, _out| {
174 if args.len() >= 2 {
175 match (&args[0], &args[1]) {
176 (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.min(y))),
177 (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.min(*y))),
178 (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).min(*y))),
179 (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.min(*y as f64))),
180 _ => Ok(VmValue::Nil),
181 }
182 } else {
183 Ok(VmValue::Nil)
184 }
185 });
186
187 vm.register_builtin("max", |args, _out| {
188 if args.len() >= 2 {
189 match (&args[0], &args[1]) {
190 (VmValue::Int(x), VmValue::Int(y)) => Ok(VmValue::Int(*x.max(y))),
191 (VmValue::Float(x), VmValue::Float(y)) => Ok(VmValue::Float(x.max(*y))),
192 (VmValue::Int(x), VmValue::Float(y)) => Ok(VmValue::Float((*x as f64).max(*y))),
193 (VmValue::Float(x), VmValue::Int(y)) => Ok(VmValue::Float(x.max(*y as f64))),
194 _ => Ok(VmValue::Nil),
195 }
196 } else {
197 Ok(VmValue::Nil)
198 }
199 });
200
201 vm.register_builtin("floor", |args, _out| {
202 match args.first().unwrap_or(&VmValue::Nil) {
203 VmValue::Float(n) => Ok(VmValue::Int(n.floor() as i64)),
204 VmValue::Int(n) => Ok(VmValue::Int(*n)),
205 _ => Ok(VmValue::Nil),
206 }
207 });
208
209 vm.register_builtin("ceil", |args, _out| {
210 match args.first().unwrap_or(&VmValue::Nil) {
211 VmValue::Float(n) => Ok(VmValue::Int(n.ceil() as i64)),
212 VmValue::Int(n) => Ok(VmValue::Int(*n)),
213 _ => Ok(VmValue::Nil),
214 }
215 });
216
217 vm.register_builtin("round", |args, _out| {
218 match args.first().unwrap_or(&VmValue::Nil) {
219 VmValue::Float(n) => Ok(VmValue::Int(n.round() as i64)),
220 VmValue::Int(n) => Ok(VmValue::Int(*n)),
221 _ => Ok(VmValue::Nil),
222 }
223 });
224
225 vm.register_builtin("sqrt", |args, _out| {
226 match args.first().unwrap_or(&VmValue::Nil) {
227 VmValue::Float(n) => Ok(VmValue::Float(n.sqrt())),
228 VmValue::Int(n) => Ok(VmValue::Float((*n as f64).sqrt())),
229 _ => Ok(VmValue::Nil),
230 }
231 });
232
233 vm.register_builtin("pow", |args, _out| {
234 if args.len() >= 2 {
235 match (&args[0], &args[1]) {
236 (VmValue::Int(base), VmValue::Int(exp)) => {
237 if *exp >= 0 && *exp <= u32::MAX as i64 {
238 Ok(VmValue::Int(base.wrapping_pow(*exp as u32)))
239 } else {
240 Ok(VmValue::Float((*base as f64).powf(*exp as f64)))
241 }
242 }
243 (VmValue::Float(base), VmValue::Int(exp)) => {
244 if *exp >= i32::MIN as i64 && *exp <= i32::MAX as i64 {
245 Ok(VmValue::Float(base.powi(*exp as i32)))
246 } else {
247 Ok(VmValue::Float(base.powf(*exp as f64)))
248 }
249 }
250 (VmValue::Int(base), VmValue::Float(exp)) => {
251 Ok(VmValue::Float((*base as f64).powf(*exp)))
252 }
253 (VmValue::Float(base), VmValue::Float(exp)) => Ok(VmValue::Float(base.powf(*exp))),
254 _ => Ok(VmValue::Nil),
255 }
256 } else {
257 Ok(VmValue::Nil)
258 }
259 });
260
261 vm.register_builtin("random", |_args, _out| {
262 use rand::Rng;
263 let val: f64 = rand::thread_rng().gen();
264 Ok(VmValue::Float(val))
265 });
266
267 vm.register_builtin("random_int", |args, _out| {
268 use rand::Rng;
269 if args.len() >= 2 {
270 let min = args[0].as_int().unwrap_or(0);
271 let max = args[1].as_int().unwrap_or(0);
272 if min <= max {
273 let val = rand::thread_rng().gen_range(min..=max);
274 return Ok(VmValue::Int(val));
275 }
276 }
277 Ok(VmValue::Nil)
278 });
279
280 vm.register_builtin("assert", |args, _out| {
283 let condition = args.first().unwrap_or(&VmValue::Nil);
284 if !condition.is_truthy() {
285 let msg = args
286 .get(1)
287 .map(|a| a.display())
288 .unwrap_or_else(|| "Assertion failed".to_string());
289 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
290 }
291 Ok(VmValue::Nil)
292 });
293
294 vm.register_builtin("assert_eq", |args, _out| {
295 if args.len() >= 2 {
296 if !values_equal(&args[0], &args[1]) {
297 let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
298 format!(
299 "Assertion failed: expected {}, got {}",
300 args[1].display(),
301 args[0].display()
302 )
303 });
304 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
305 }
306 Ok(VmValue::Nil)
307 } else {
308 Err(VmError::Thrown(VmValue::String(Rc::from(
309 "assert_eq requires at least 2 arguments",
310 ))))
311 }
312 });
313
314 vm.register_builtin("assert_ne", |args, _out| {
315 if args.len() >= 2 {
316 if values_equal(&args[0], &args[1]) {
317 let msg = args.get(2).map(|a| a.display()).unwrap_or_else(|| {
318 format!(
319 "Assertion failed: values should not be equal: {}",
320 args[0].display()
321 )
322 });
323 return Err(VmError::Thrown(VmValue::String(Rc::from(msg))));
324 }
325 Ok(VmValue::Nil)
326 } else {
327 Err(VmError::Thrown(VmValue::String(Rc::from(
328 "assert_ne requires at least 2 arguments",
329 ))))
330 }
331 });
332
333 vm.register_builtin("__range__", |args, _out| {
334 let start = args.first().and_then(|a| a.as_int()).unwrap_or(0);
335 let end = args.get(1).and_then(|a| a.as_int()).unwrap_or(0);
336 let inclusive = args.get(2).map(|a| a.is_truthy()).unwrap_or(false);
337 let items: Vec<VmValue> = if inclusive {
338 (start..=end).map(VmValue::Int).collect()
339 } else {
340 (start..end).map(VmValue::Int).collect()
341 };
342 Ok(VmValue::List(Rc::new(items)))
343 });
344
345 vm.register_builtin("file_exists", |args, _out| {
350 let path = args.first().map(|a| a.display()).unwrap_or_default();
351 Ok(VmValue::Bool(std::path::Path::new(&path).exists()))
352 });
353
354 vm.register_builtin("delete_file", |args, _out| {
355 let path = args.first().map(|a| a.display()).unwrap_or_default();
356 let p = std::path::Path::new(&path);
357 if p.is_dir() {
358 std::fs::remove_dir_all(&path).map_err(|e| {
359 VmError::Thrown(VmValue::String(Rc::from(format!(
360 "Failed to delete directory {path}: {e}"
361 ))))
362 })?;
363 } else {
364 std::fs::remove_file(&path).map_err(|e| {
365 VmError::Thrown(VmValue::String(Rc::from(format!(
366 "Failed to delete file {path}: {e}"
367 ))))
368 })?;
369 }
370 Ok(VmValue::Nil)
371 });
372
373 vm.register_builtin("append_file", |args, _out| {
374 use std::io::Write;
375 if args.len() >= 2 {
376 let path = args[0].display();
377 let content = args[1].display();
378 let mut file = std::fs::OpenOptions::new()
379 .append(true)
380 .create(true)
381 .open(&path)
382 .map_err(|e| {
383 VmError::Thrown(VmValue::String(Rc::from(format!(
384 "Failed to open file {path}: {e}"
385 ))))
386 })?;
387 file.write_all(content.as_bytes()).map_err(|e| {
388 VmError::Thrown(VmValue::String(Rc::from(format!(
389 "Failed to append to file {path}: {e}"
390 ))))
391 })?;
392 }
393 Ok(VmValue::Nil)
394 });
395
396 vm.register_builtin("list_dir", |args, _out| {
397 let path = args
398 .first()
399 .map(|a| a.display())
400 .unwrap_or_else(|| ".".to_string());
401 let entries = std::fs::read_dir(&path).map_err(|e| {
402 VmError::Thrown(VmValue::String(Rc::from(format!(
403 "Failed to list directory {path}: {e}"
404 ))))
405 })?;
406 let mut result = Vec::new();
407 for entry in entries {
408 let entry =
409 entry.map_err(|e| VmError::Thrown(VmValue::String(Rc::from(e.to_string()))))?;
410 let name = entry.file_name().to_string_lossy().to_string();
411 result.push(VmValue::String(Rc::from(name.as_str())));
412 }
413 result.sort_by_key(|a| a.display());
414 Ok(VmValue::List(Rc::new(result)))
415 });
416
417 vm.register_builtin("mkdir", |args, _out| {
418 let path = args.first().map(|a| a.display()).unwrap_or_default();
419 std::fs::create_dir_all(&path).map_err(|e| {
420 VmError::Thrown(VmValue::String(Rc::from(format!(
421 "Failed to create directory {path}: {e}"
422 ))))
423 })?;
424 Ok(VmValue::Nil)
425 });
426
427 vm.register_builtin("path_join", |args, _out| {
428 let mut path = std::path::PathBuf::new();
429 for arg in args {
430 path.push(arg.display());
431 }
432 Ok(VmValue::String(Rc::from(
433 path.to_string_lossy().to_string().as_str(),
434 )))
435 });
436
437 vm.register_builtin("copy_file", |args, _out| {
438 if args.len() >= 2 {
439 let src = args[0].display();
440 let dst = args[1].display();
441 std::fs::copy(&src, &dst).map_err(|e| {
442 VmError::Thrown(VmValue::String(Rc::from(format!(
443 "Failed to copy {src} to {dst}: {e}"
444 ))))
445 })?;
446 }
447 Ok(VmValue::Nil)
448 });
449
450 vm.register_builtin("temp_dir", |_args, _out| {
451 Ok(VmValue::String(Rc::from(
452 std::env::temp_dir().to_string_lossy().to_string().as_str(),
453 )))
454 });
455
456 vm.register_builtin("stat", |args, _out| {
457 let path = args.first().map(|a| a.display()).unwrap_or_default();
458 let metadata = std::fs::metadata(&path).map_err(|e| {
459 VmError::Thrown(VmValue::String(Rc::from(format!(
460 "Failed to stat {path}: {e}"
461 ))))
462 })?;
463 let mut info = BTreeMap::new();
464 info.insert("size".to_string(), VmValue::Int(metadata.len() as i64));
465 info.insert("is_file".to_string(), VmValue::Bool(metadata.is_file()));
466 info.insert("is_dir".to_string(), VmValue::Bool(metadata.is_dir()));
467 info.insert(
468 "readonly".to_string(),
469 VmValue::Bool(metadata.permissions().readonly()),
470 );
471 if let Ok(modified) = metadata.modified() {
472 if let Ok(dur) = modified.duration_since(std::time::UNIX_EPOCH) {
473 info.insert("modified".to_string(), VmValue::Float(dur.as_secs_f64()));
474 }
475 }
476 Ok(VmValue::Dict(Rc::new(info)))
477 });
478
479 vm.register_builtin("exec", |args, _out| {
484 if args.is_empty() {
485 return Err(VmError::Thrown(VmValue::String(Rc::from(
486 "exec: command is required",
487 ))));
488 }
489 let cmd = args[0].display();
490 let cmd_args: Vec<String> = args[1..].iter().map(|a| a.display()).collect();
491 let output = std::process::Command::new(&cmd)
492 .args(&cmd_args)
493 .output()
494 .map_err(|e| VmError::Thrown(VmValue::String(Rc::from(format!("exec failed: {e}")))))?;
495 Ok(vm_output_to_value(output))
496 });
497
498 vm.register_builtin("shell", |args, _out| {
499 let cmd = args.first().map(|a| a.display()).unwrap_or_default();
500 if cmd.is_empty() {
501 return Err(VmError::Thrown(VmValue::String(Rc::from(
502 "shell: command string is required",
503 ))));
504 }
505 let shell = if cfg!(target_os = "windows") {
506 "cmd"
507 } else {
508 "sh"
509 };
510 let flag = if cfg!(target_os = "windows") {
511 "/C"
512 } else {
513 "-c"
514 };
515 let output = std::process::Command::new(shell)
516 .arg(flag)
517 .arg(&cmd)
518 .output()
519 .map_err(|e| {
520 VmError::Thrown(VmValue::String(Rc::from(format!("shell failed: {e}"))))
521 })?;
522 Ok(vm_output_to_value(output))
523 });
524
525 vm.register_builtin("date_now", |_args, _out| {
530 use std::time::{SystemTime, UNIX_EPOCH};
531 let now = SystemTime::now()
532 .duration_since(UNIX_EPOCH)
533 .unwrap_or_default();
534 let total_secs = now.as_secs();
535 let (y, m, d, hour, minute, second, dow) = vm_civil_from_timestamp(total_secs);
536 let mut result = BTreeMap::new();
537 result.insert("year".to_string(), VmValue::Int(y));
538 result.insert("month".to_string(), VmValue::Int(m));
539 result.insert("day".to_string(), VmValue::Int(d));
540 result.insert("hour".to_string(), VmValue::Int(hour));
541 result.insert("minute".to_string(), VmValue::Int(minute));
542 result.insert("second".to_string(), VmValue::Int(second));
543 result.insert("weekday".to_string(), VmValue::Int(dow));
544 result.insert("timestamp".to_string(), VmValue::Float(now.as_secs_f64()));
545 Ok(VmValue::Dict(Rc::new(result)))
546 });
547
548 vm.register_builtin("date_format", |args, _out| {
549 let ts = match args.first() {
550 Some(VmValue::Float(f)) => *f,
551 Some(VmValue::Int(n)) => *n as f64,
552 Some(VmValue::Dict(map)) => map
553 .get("timestamp")
554 .and_then(|v| match v {
555 VmValue::Float(f) => Some(*f),
556 VmValue::Int(n) => Some(*n as f64),
557 _ => None,
558 })
559 .unwrap_or(0.0),
560 _ => 0.0,
561 };
562 let fmt = args
563 .get(1)
564 .map(|a| a.display())
565 .unwrap_or_else(|| "%Y-%m-%d %H:%M:%S".to_string());
566
567 let (y, m, d, hour, minute, second, _dow) = vm_civil_from_timestamp(ts as u64);
568
569 let result = fmt
570 .replace("%Y", &format!("{y:04}"))
571 .replace("%m", &format!("{m:02}"))
572 .replace("%d", &format!("{d:02}"))
573 .replace("%H", &format!("{hour:02}"))
574 .replace("%M", &format!("{minute:02}"))
575 .replace("%S", &format!("{second:02}"));
576
577 Ok(VmValue::String(Rc::from(result.as_str())))
578 });
579
580 vm.register_builtin("date_parse", |args, _out| {
581 let s = args.first().map(|a| a.display()).unwrap_or_default();
582 let parts: Vec<&str> = s.split(|c: char| !c.is_ascii_digit()).collect();
583 let parts: Vec<i64> = parts.iter().filter_map(|p| p.parse().ok()).collect();
584 if parts.len() < 3 {
585 return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
586 "Cannot parse date: {s}"
587 )))));
588 }
589 let (y, m, d) = (parts[0], parts[1], parts[2]);
590 let hour = parts.get(3).copied().unwrap_or(0);
591 let minute = parts.get(4).copied().unwrap_or(0);
592 let second = parts.get(5).copied().unwrap_or(0);
593
594 let (y_adj, m_adj) = if m <= 2 {
595 (y - 1, (m + 9) as u64)
596 } else {
597 (y, (m - 3) as u64)
598 };
599 let era = if y_adj >= 0 { y_adj } else { y_adj - 399 } / 400;
600 let yoe = (y_adj - era * 400) as u64;
601 let doy = (153 * m_adj + 2) / 5 + d as u64 - 1;
602 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
603 let days = era * 146097 + doe as i64 - 719468;
604 let ts = days * 86400 + hour * 3600 + minute * 60 + second;
605 Ok(VmValue::Float(ts as f64))
606 });
607
608 vm.register_builtin("format", |args, _out| {
613 let template = args.first().map(|a| a.display()).unwrap_or_default();
614 let mut result = String::with_capacity(template.len());
615 let mut arg_iter = args.iter().skip(1);
616 let mut rest = template.as_str();
617 while let Some(pos) = rest.find("{}") {
618 result.push_str(&rest[..pos]);
619 if let Some(arg) = arg_iter.next() {
620 result.push_str(&arg.display());
621 } else {
622 result.push_str("{}");
623 }
624 rest = &rest[pos + 2..];
625 }
626 result.push_str(rest);
627 Ok(VmValue::String(Rc::from(result.as_str())))
628 });
629
630 vm.register_builtin("trim", |args, _out| {
635 let s = args.first().map(|a| a.display()).unwrap_or_default();
636 Ok(VmValue::String(Rc::from(s.trim())))
637 });
638
639 vm.register_builtin("lowercase", |args, _out| {
640 let s = args.first().map(|a| a.display()).unwrap_or_default();
641 Ok(VmValue::String(Rc::from(s.to_lowercase().as_str())))
642 });
643
644 vm.register_builtin("uppercase", |args, _out| {
645 let s = args.first().map(|a| a.display()).unwrap_or_default();
646 Ok(VmValue::String(Rc::from(s.to_uppercase().as_str())))
647 });
648
649 vm.register_builtin("split", |args, _out| {
650 let s = args.first().map(|a| a.display()).unwrap_or_default();
651 let sep = args
652 .get(1)
653 .map(|a| a.display())
654 .unwrap_or_else(|| " ".to_string());
655 let parts: Vec<VmValue> = s
656 .split(&sep)
657 .map(|p| VmValue::String(Rc::from(p)))
658 .collect();
659 Ok(VmValue::List(Rc::new(parts)))
660 });
661
662 vm.register_builtin("starts_with", |args, _out| {
663 let s = args.first().map(|a| a.display()).unwrap_or_default();
664 let prefix = args.get(1).map(|a| a.display()).unwrap_or_default();
665 Ok(VmValue::Bool(s.starts_with(&prefix)))
666 });
667
668 vm.register_builtin("ends_with", |args, _out| {
669 let s = args.first().map(|a| a.display()).unwrap_or_default();
670 let suffix = args.get(1).map(|a| a.display()).unwrap_or_default();
671 Ok(VmValue::Bool(s.ends_with(&suffix)))
672 });
673
674 vm.register_builtin("contains", |args, _out| {
675 match args.first().unwrap_or(&VmValue::Nil) {
676 VmValue::String(s) => {
677 let substr = args.get(1).map(|a| a.display()).unwrap_or_default();
678 Ok(VmValue::Bool(s.contains(&substr)))
679 }
680 VmValue::List(items) => {
681 let target = args.get(1).unwrap_or(&VmValue::Nil);
682 Ok(VmValue::Bool(
683 items.iter().any(|item| values_equal(item, target)),
684 ))
685 }
686 _ => Ok(VmValue::Bool(false)),
687 }
688 });
689
690 vm.register_builtin("replace", |args, _out| {
691 let s = args.first().map(|a| a.display()).unwrap_or_default();
692 let old = args.get(1).map(|a| a.display()).unwrap_or_default();
693 let new = args.get(2).map(|a| a.display()).unwrap_or_default();
694 Ok(VmValue::String(Rc::from(s.replace(&old, &new).as_str())))
695 });
696
697 vm.register_builtin("join", |args, _out| {
698 let sep = args.get(1).map(|a| a.display()).unwrap_or_default();
699 match args.first() {
700 Some(VmValue::List(items)) => {
701 let parts: Vec<String> = items.iter().map(|v| v.display()).collect();
702 Ok(VmValue::String(Rc::from(parts.join(&sep).as_str())))
703 }
704 _ => Ok(VmValue::String(Rc::from(""))),
705 }
706 });
707
708 vm.register_builtin("len", |args, _out| {
709 match args.first().unwrap_or(&VmValue::Nil) {
710 VmValue::String(s) => Ok(VmValue::Int(s.len() as i64)),
711 VmValue::List(items) => Ok(VmValue::Int(items.len() as i64)),
712 VmValue::Dict(map) => Ok(VmValue::Int(map.len() as i64)),
713 _ => Ok(VmValue::Int(0)),
714 }
715 });
716
717 vm.register_builtin("substring", |args, _out| {
718 let s = args.first().map(|a| a.display()).unwrap_or_default();
719 let start = args.get(1).and_then(|a| a.as_int()).unwrap_or(0) as usize;
720 let start = start.min(s.len());
721 match args.get(2).and_then(|a| a.as_int()) {
722 Some(length) => {
723 let length = (length as usize).min(s.len() - start);
724 Ok(VmValue::String(Rc::from(&s[start..start + length])))
725 }
726 None => Ok(VmValue::String(Rc::from(&s[start..]))),
727 }
728 });
729
730 vm.register_builtin("dirname", |args, _out| {
735 let path = args.first().map(|a| a.display()).unwrap_or_default();
736 let p = std::path::Path::new(&path);
737 match p.parent() {
738 Some(parent) => Ok(VmValue::String(Rc::from(parent.to_string_lossy().as_ref()))),
739 None => Ok(VmValue::String(Rc::from(""))),
740 }
741 });
742
743 vm.register_builtin("basename", |args, _out| {
744 let path = args.first().map(|a| a.display()).unwrap_or_default();
745 let p = std::path::Path::new(&path);
746 match p.file_name() {
747 Some(name) => Ok(VmValue::String(Rc::from(name.to_string_lossy().as_ref()))),
748 None => Ok(VmValue::String(Rc::from(""))),
749 }
750 });
751
752 vm.register_builtin("extname", |args, _out| {
753 let path = args.first().map(|a| a.display()).unwrap_or_default();
754 let p = std::path::Path::new(&path);
755 match p.extension() {
756 Some(ext) => Ok(VmValue::String(Rc::from(
757 format!(".{}", ext.to_string_lossy()).as_str(),
758 ))),
759 None => Ok(VmValue::String(Rc::from(""))),
760 }
761 });
762
763 vm.register_builtin("log_debug", |args, out| {
768 vm_write_log("debug", 0, args, out);
769 Ok(VmValue::Nil)
770 });
771
772 vm.register_builtin("log_info", |args, out| {
773 vm_write_log("info", 1, args, out);
774 Ok(VmValue::Nil)
775 });
776
777 vm.register_builtin("log_warn", |args, out| {
778 vm_write_log("warn", 2, args, out);
779 Ok(VmValue::Nil)
780 });
781
782 vm.register_builtin("log_error", |args, out| {
783 vm_write_log("error", 3, args, out);
784 Ok(VmValue::Nil)
785 });
786
787 vm.register_builtin("log_set_level", |args, _out| {
788 let level_str = args.first().map(|a| a.display()).unwrap_or_default();
789 match vm_level_to_u8(&level_str) {
790 Some(n) => {
791 VM_MIN_LOG_LEVEL.store(n, Ordering::Relaxed);
792 Ok(VmValue::Nil)
793 }
794 None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
795 "log_set_level: invalid level '{}'. Expected debug, info, warn, or error",
796 level_str
797 ))))),
798 }
799 });
800
801 vm.register_builtin("trace_start", |args, _out| {
806 use rand::Rng;
807 let name = args.first().map(|a| a.display()).unwrap_or_default();
808 let trace_id = VM_TRACE_STACK.with(|stack| {
809 stack
810 .borrow()
811 .last()
812 .map(|t| t.trace_id.clone())
813 .unwrap_or_else(|| {
814 let val: u32 = rand::thread_rng().gen();
815 format!("{val:08x}")
816 })
817 });
818 let span_id = {
819 let val: u32 = rand::thread_rng().gen();
820 format!("{val:08x}")
821 };
822 let start_ms = std::time::SystemTime::now()
823 .duration_since(std::time::UNIX_EPOCH)
824 .unwrap_or_default()
825 .as_millis() as i64;
826
827 VM_TRACE_STACK.with(|stack| {
828 stack.borrow_mut().push(VmTraceContext {
829 trace_id: trace_id.clone(),
830 span_id: span_id.clone(),
831 });
832 });
833
834 let mut span = BTreeMap::new();
835 span.insert(
836 "trace_id".to_string(),
837 VmValue::String(Rc::from(trace_id.as_str())),
838 );
839 span.insert(
840 "span_id".to_string(),
841 VmValue::String(Rc::from(span_id.as_str())),
842 );
843 span.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
844 span.insert("start_ms".to_string(), VmValue::Int(start_ms));
845 Ok(VmValue::Dict(Rc::new(span)))
846 });
847
848 vm.register_builtin("trace_end", |args, out| {
849 let span = match args.first() {
850 Some(VmValue::Dict(d)) => d,
851 _ => {
852 return Err(VmError::Thrown(VmValue::String(Rc::from(
853 "trace_end: argument must be a span dict from trace_start",
854 ))));
855 }
856 };
857
858 let end_ms = std::time::SystemTime::now()
859 .duration_since(std::time::UNIX_EPOCH)
860 .unwrap_or_default()
861 .as_millis() as i64;
862
863 let start_ms = span
864 .get("start_ms")
865 .and_then(|v| v.as_int())
866 .unwrap_or(end_ms);
867 let duration_ms = end_ms - start_ms;
868 let name = span.get("name").map(|v| v.display()).unwrap_or_default();
869 let trace_id = span
870 .get("trace_id")
871 .map(|v| v.display())
872 .unwrap_or_default();
873 let span_id = span.get("span_id").map(|v| v.display()).unwrap_or_default();
874
875 VM_TRACE_STACK.with(|stack| {
876 stack.borrow_mut().pop();
877 });
878
879 let level_num = 1_u8;
880 if level_num >= VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
881 let mut fields = BTreeMap::new();
882 fields.insert(
883 "trace_id".to_string(),
884 VmValue::String(Rc::from(trace_id.as_str())),
885 );
886 fields.insert(
887 "span_id".to_string(),
888 VmValue::String(Rc::from(span_id.as_str())),
889 );
890 fields.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
891 fields.insert("duration_ms".to_string(), VmValue::Int(duration_ms));
892 let line = vm_build_log_line("info", "span_end", Some(&fields));
893 out.push_str(&line);
894 }
895
896 Ok(VmValue::Nil)
897 });
898
899 vm.register_builtin("trace_id", |_args, _out| {
900 let id = VM_TRACE_STACK.with(|stack| stack.borrow().last().map(|t| t.trace_id.clone()));
901 match id {
902 Some(trace_id) => Ok(VmValue::String(Rc::from(trace_id.as_str()))),
903 None => Ok(VmValue::Nil),
904 }
905 });
906
907 vm.register_builtin("tool_registry", |_args, _out| {
912 let mut registry = BTreeMap::new();
913 registry.insert(
914 "_type".to_string(),
915 VmValue::String(Rc::from("tool_registry")),
916 );
917 registry.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
918 Ok(VmValue::Dict(Rc::new(registry)))
919 });
920
921 vm.register_builtin("tool_add", |args, _out| {
922 if args.len() < 4 {
923 return Err(VmError::Thrown(VmValue::String(Rc::from(
924 "tool_add: requires registry, name, description, and handler",
925 ))));
926 }
927
928 let registry = match &args[0] {
929 VmValue::Dict(map) => (**map).clone(),
930 _ => {
931 return Err(VmError::Thrown(VmValue::String(Rc::from(
932 "tool_add: first argument must be a tool registry",
933 ))));
934 }
935 };
936
937 match registry.get("_type") {
938 Some(VmValue::String(t)) if &**t == "tool_registry" => {}
939 _ => {
940 return Err(VmError::Thrown(VmValue::String(Rc::from(
941 "tool_add: first argument must be a tool registry",
942 ))));
943 }
944 }
945
946 let name = args[1].display();
947 let description = args[2].display();
948 let handler = args[3].clone();
949 let parameters = if args.len() > 4 {
950 args[4].clone()
951 } else {
952 VmValue::Dict(Rc::new(BTreeMap::new()))
953 };
954
955 let mut tool_entry = BTreeMap::new();
956 tool_entry.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
957 tool_entry.insert(
958 "description".to_string(),
959 VmValue::String(Rc::from(description.as_str())),
960 );
961 tool_entry.insert("handler".to_string(), handler);
962 tool_entry.insert("parameters".to_string(), parameters);
963
964 let mut tools: Vec<VmValue> = match registry.get("tools") {
965 Some(VmValue::List(list)) => list
966 .iter()
967 .filter(|t| {
968 if let VmValue::Dict(e) = t {
969 e.get("name").map(|v| v.display()).as_deref() != Some(name.as_str())
970 } else {
971 true
972 }
973 })
974 .cloned()
975 .collect(),
976 _ => Vec::new(),
977 };
978 tools.push(VmValue::Dict(Rc::new(tool_entry)));
979
980 let mut new_registry = registry;
981 new_registry.insert("tools".to_string(), VmValue::List(Rc::new(tools)));
982 Ok(VmValue::Dict(Rc::new(new_registry)))
983 });
984
985 vm.register_builtin("tool_list", |args, _out| {
986 let registry = match args.first() {
987 Some(VmValue::Dict(map)) => map,
988 _ => {
989 return Err(VmError::Thrown(VmValue::String(Rc::from(
990 "tool_list: requires a tool registry",
991 ))));
992 }
993 };
994 vm_validate_registry("tool_list", registry)?;
995
996 let tools = vm_get_tools(registry);
997 let mut result = Vec::new();
998 for tool in tools {
999 if let VmValue::Dict(entry) = tool {
1000 let mut desc = BTreeMap::new();
1001 if let Some(name) = entry.get("name") {
1002 desc.insert("name".to_string(), name.clone());
1003 }
1004 if let Some(description) = entry.get("description") {
1005 desc.insert("description".to_string(), description.clone());
1006 }
1007 if let Some(parameters) = entry.get("parameters") {
1008 desc.insert("parameters".to_string(), parameters.clone());
1009 }
1010 result.push(VmValue::Dict(Rc::new(desc)));
1011 }
1012 }
1013 Ok(VmValue::List(Rc::new(result)))
1014 });
1015
1016 vm.register_builtin("tool_find", |args, _out| {
1017 if args.len() < 2 {
1018 return Err(VmError::Thrown(VmValue::String(Rc::from(
1019 "tool_find: requires registry and name",
1020 ))));
1021 }
1022
1023 let registry = match &args[0] {
1024 VmValue::Dict(map) => map,
1025 _ => {
1026 return Err(VmError::Thrown(VmValue::String(Rc::from(
1027 "tool_find: first argument must be a tool registry",
1028 ))));
1029 }
1030 };
1031 vm_validate_registry("tool_find", registry)?;
1032
1033 let target_name = args[1].display();
1034 let tools = vm_get_tools(registry);
1035
1036 for tool in tools {
1037 if let VmValue::Dict(entry) = tool {
1038 if let Some(VmValue::String(name)) = entry.get("name") {
1039 if &**name == target_name.as_str() {
1040 return Ok(tool.clone());
1041 }
1042 }
1043 }
1044 }
1045 Ok(VmValue::Nil)
1046 });
1047
1048 vm.register_builtin("tool_describe", |args, _out| {
1049 let registry = match args.first() {
1050 Some(VmValue::Dict(map)) => map,
1051 _ => {
1052 return Err(VmError::Thrown(VmValue::String(Rc::from(
1053 "tool_describe: requires a tool registry",
1054 ))));
1055 }
1056 };
1057 vm_validate_registry("tool_describe", registry)?;
1058
1059 let tools = vm_get_tools(registry);
1060
1061 if tools.is_empty() {
1062 return Ok(VmValue::String(Rc::from("Available tools:\n(none)")));
1063 }
1064
1065 let mut tool_infos: Vec<(String, String, String)> = Vec::new();
1066 for tool in tools {
1067 if let VmValue::Dict(entry) = tool {
1068 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1069 let description = entry
1070 .get("description")
1071 .map(|v| v.display())
1072 .unwrap_or_default();
1073 let params_str = vm_format_parameters(entry.get("parameters"));
1074 tool_infos.push((name, params_str, description));
1075 }
1076 }
1077
1078 tool_infos.sort_by(|a, b| a.0.cmp(&b.0));
1079
1080 let mut lines = vec!["Available tools:".to_string()];
1081 for (name, params, desc) in &tool_infos {
1082 lines.push(format!("- {name}({params}): {desc}"));
1083 }
1084
1085 Ok(VmValue::String(Rc::from(lines.join("\n").as_str())))
1086 });
1087
1088 vm.register_builtin("tool_remove", |args, _out| {
1089 if args.len() < 2 {
1090 return Err(VmError::Thrown(VmValue::String(Rc::from(
1091 "tool_remove: requires registry and name",
1092 ))));
1093 }
1094
1095 let registry = match &args[0] {
1096 VmValue::Dict(map) => (**map).clone(),
1097 _ => {
1098 return Err(VmError::Thrown(VmValue::String(Rc::from(
1099 "tool_remove: first argument must be a tool registry",
1100 ))));
1101 }
1102 };
1103 vm_validate_registry("tool_remove", ®istry)?;
1104
1105 let target_name = args[1].display();
1106
1107 let tools = match registry.get("tools") {
1108 Some(VmValue::List(list)) => (**list).clone(),
1109 _ => Vec::new(),
1110 };
1111
1112 let filtered: Vec<VmValue> = tools
1113 .into_iter()
1114 .filter(|tool| {
1115 if let VmValue::Dict(entry) = tool {
1116 if let Some(VmValue::String(name)) = entry.get("name") {
1117 return &**name != target_name.as_str();
1118 }
1119 }
1120 true
1121 })
1122 .collect();
1123
1124 let mut new_registry = registry;
1125 new_registry.insert("tools".to_string(), VmValue::List(Rc::new(filtered)));
1126 Ok(VmValue::Dict(Rc::new(new_registry)))
1127 });
1128
1129 vm.register_builtin("tool_count", |args, _out| {
1130 let registry = match args.first() {
1131 Some(VmValue::Dict(map)) => map,
1132 _ => {
1133 return Err(VmError::Thrown(VmValue::String(Rc::from(
1134 "tool_count: requires a tool registry",
1135 ))));
1136 }
1137 };
1138 vm_validate_registry("tool_count", registry)?;
1139 let count = vm_get_tools(registry).len();
1140 Ok(VmValue::Int(count as i64))
1141 });
1142
1143 vm.register_builtin("tool_schema", |args, _out| {
1144 let registry = match args.first() {
1145 Some(VmValue::Dict(map)) => {
1146 vm_validate_registry("tool_schema", map)?;
1147 map
1148 }
1149 _ => {
1150 return Err(VmError::Thrown(VmValue::String(Rc::from(
1151 "tool_schema: requires a tool registry",
1152 ))));
1153 }
1154 };
1155
1156 let components = args.get(1).and_then(|v| v.as_dict()).cloned();
1157
1158 let tools = match registry.get("tools") {
1159 Some(VmValue::List(list)) => list,
1160 _ => return Ok(VmValue::Dict(Rc::new(vm_build_empty_schema()))),
1161 };
1162
1163 let mut tool_schemas = Vec::new();
1164 for tool in tools.iter() {
1165 if let VmValue::Dict(entry) = tool {
1166 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1167 let description = entry
1168 .get("description")
1169 .map(|v| v.display())
1170 .unwrap_or_default();
1171
1172 let input_schema =
1173 vm_build_input_schema(entry.get("parameters"), components.as_ref());
1174
1175 let mut tool_def = BTreeMap::new();
1176 tool_def.insert("name".to_string(), VmValue::String(Rc::from(name.as_str())));
1177 tool_def.insert(
1178 "description".to_string(),
1179 VmValue::String(Rc::from(description.as_str())),
1180 );
1181 tool_def.insert("inputSchema".to_string(), input_schema);
1182 tool_schemas.push(VmValue::Dict(Rc::new(tool_def)));
1183 }
1184 }
1185
1186 let mut schema = BTreeMap::new();
1187 schema.insert(
1188 "schema_version".to_string(),
1189 VmValue::String(Rc::from("harn-tools/1.0")),
1190 );
1191
1192 if let Some(comps) = &components {
1193 let mut comp_wrapper = BTreeMap::new();
1194 comp_wrapper.insert("schemas".to_string(), VmValue::Dict(Rc::new(comps.clone())));
1195 schema.insert(
1196 "components".to_string(),
1197 VmValue::Dict(Rc::new(comp_wrapper)),
1198 );
1199 }
1200
1201 schema.insert("tools".to_string(), VmValue::List(Rc::new(tool_schemas)));
1202 Ok(VmValue::Dict(Rc::new(schema)))
1203 });
1204
1205 vm.register_builtin("tool_parse_call", |args, _out| {
1206 let text = args.first().map(|a| a.display()).unwrap_or_default();
1207
1208 let mut results = Vec::new();
1209 let mut search_from = 0;
1210
1211 while let Some(start) = text[search_from..].find("<tool_call>") {
1212 let abs_start = search_from + start + "<tool_call>".len();
1213 if let Some(end) = text[abs_start..].find("</tool_call>") {
1214 let json_str = text[abs_start..abs_start + end].trim();
1215 if let Ok(jv) = serde_json::from_str::<serde_json::Value>(json_str) {
1216 results.push(json_to_vm_value(&jv));
1217 }
1218 search_from = abs_start + end + "</tool_call>".len();
1219 } else {
1220 break;
1221 }
1222 }
1223
1224 Ok(VmValue::List(Rc::new(results)))
1225 });
1226
1227 vm.register_builtin("tool_format_result", |args, _out| {
1228 if args.len() < 2 {
1229 return Err(VmError::Thrown(VmValue::String(Rc::from(
1230 "tool_format_result: requires name and result",
1231 ))));
1232 }
1233 let name = args[0].display();
1234 let result = args[1].display();
1235
1236 let json_name = vm_escape_json_str(&name);
1237 let json_result = vm_escape_json_str(&result);
1238 Ok(VmValue::String(Rc::from(
1239 format!(
1240 "<tool_result>{{\"name\": \"{json_name}\", \"result\": \"{json_result}\"}}</tool_result>"
1241 )
1242 .as_str(),
1243 )))
1244 });
1245
1246 vm.register_builtin("tool_prompt", |args, _out| {
1247 let registry = match args.first() {
1248 Some(VmValue::Dict(map)) => {
1249 vm_validate_registry("tool_prompt", map)?;
1250 map
1251 }
1252 _ => {
1253 return Err(VmError::Thrown(VmValue::String(Rc::from(
1254 "tool_prompt: requires a tool registry",
1255 ))));
1256 }
1257 };
1258
1259 let tools = match registry.get("tools") {
1260 Some(VmValue::List(list)) => list,
1261 _ => {
1262 return Ok(VmValue::String(Rc::from("No tools are available.")));
1263 }
1264 };
1265
1266 if tools.is_empty() {
1267 return Ok(VmValue::String(Rc::from("No tools are available.")));
1268 }
1269
1270 let mut prompt = String::from("# Available Tools\n\n");
1271 prompt.push_str("You have access to the following tools. To use a tool, output a tool call in this exact format:\n\n");
1272 prompt.push_str("<tool_call>{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}</tool_call>\n\n");
1273 prompt.push_str("You may make multiple tool calls in a single response. Wait for tool results before proceeding.\n\n");
1274 prompt.push_str("## Tools\n\n");
1275
1276 let mut tool_infos: Vec<(&BTreeMap<String, VmValue>, String)> = Vec::new();
1277 for tool in tools.iter() {
1278 if let VmValue::Dict(entry) = tool {
1279 let name = entry.get("name").map(|v| v.display()).unwrap_or_default();
1280 tool_infos.push((entry, name));
1281 }
1282 }
1283 tool_infos.sort_by(|a, b| a.1.cmp(&b.1));
1284
1285 for (entry, name) in &tool_infos {
1286 let description = entry
1287 .get("description")
1288 .map(|v| v.display())
1289 .unwrap_or_default();
1290 let params_str = vm_format_parameters(entry.get("parameters"));
1291
1292 prompt.push_str(&format!("### {name}\n"));
1293 prompt.push_str(&format!("{description}\n"));
1294 if !params_str.is_empty() {
1295 prompt.push_str(&format!("Parameters: {params_str}\n"));
1296 }
1297 prompt.push('\n');
1298 }
1299
1300 Ok(VmValue::String(Rc::from(prompt.trim_end())))
1301 });
1302
1303 vm.register_builtin("channel", |args, _out| {
1308 let name = args
1309 .first()
1310 .map(|a| a.display())
1311 .unwrap_or_else(|| "default".to_string());
1312 let capacity = args.get(1).and_then(|a| a.as_int()).unwrap_or(256) as usize;
1313 let capacity = capacity.max(1);
1314 let (tx, rx) = tokio::sync::mpsc::channel(capacity);
1315 #[allow(clippy::arc_with_non_send_sync)]
1316 Ok(VmValue::Channel(VmChannelHandle {
1317 name,
1318 sender: Arc::new(tx),
1319 receiver: Arc::new(tokio::sync::Mutex::new(rx)),
1320 closed: Arc::new(AtomicBool::new(false)),
1321 }))
1322 });
1323
1324 vm.register_builtin("close_channel", |args, _out| {
1325 if args.is_empty() {
1326 return Err(VmError::Thrown(VmValue::String(Rc::from(
1327 "close_channel: requires a channel",
1328 ))));
1329 }
1330 if let VmValue::Channel(ch) = &args[0] {
1331 ch.closed.store(true, Ordering::SeqCst);
1332 Ok(VmValue::Nil)
1333 } else {
1334 Err(VmError::Thrown(VmValue::String(Rc::from(
1335 "close_channel: first argument must be a channel",
1336 ))))
1337 }
1338 });
1339
1340 vm.register_builtin("try_receive", |args, _out| {
1341 if args.is_empty() {
1342 return Err(VmError::Thrown(VmValue::String(Rc::from(
1343 "try_receive: requires a channel",
1344 ))));
1345 }
1346 if let VmValue::Channel(ch) = &args[0] {
1347 match ch.receiver.try_lock() {
1348 Ok(mut rx) => match rx.try_recv() {
1349 Ok(val) => Ok(val),
1350 Err(_) => Ok(VmValue::Nil),
1351 },
1352 Err(_) => Ok(VmValue::Nil),
1353 }
1354 } else {
1355 Err(VmError::Thrown(VmValue::String(Rc::from(
1356 "try_receive: first argument must be a channel",
1357 ))))
1358 }
1359 });
1360
1361 vm.register_builtin("atomic", |args, _out| {
1366 let initial = match args.first() {
1367 Some(VmValue::Int(n)) => *n,
1368 Some(VmValue::Float(f)) => *f as i64,
1369 Some(VmValue::Bool(b)) => {
1370 if *b {
1371 1
1372 } else {
1373 0
1374 }
1375 }
1376 _ => 0,
1377 };
1378 Ok(VmValue::Atomic(VmAtomicHandle {
1379 value: Arc::new(AtomicI64::new(initial)),
1380 }))
1381 });
1382
1383 vm.register_builtin("atomic_get", |args, _out| {
1384 if let Some(VmValue::Atomic(a)) = args.first() {
1385 Ok(VmValue::Int(a.value.load(Ordering::SeqCst)))
1386 } else {
1387 Ok(VmValue::Nil)
1388 }
1389 });
1390
1391 vm.register_builtin("atomic_set", |args, _out| {
1392 if args.len() >= 2 {
1393 if let (VmValue::Atomic(a), Some(val)) = (&args[0], args[1].as_int()) {
1394 let old = a.value.swap(val, Ordering::SeqCst);
1395 return Ok(VmValue::Int(old));
1396 }
1397 }
1398 Ok(VmValue::Nil)
1399 });
1400
1401 vm.register_builtin("atomic_add", |args, _out| {
1402 if args.len() >= 2 {
1403 if let (VmValue::Atomic(a), Some(delta)) = (&args[0], args[1].as_int()) {
1404 let prev = a.value.fetch_add(delta, Ordering::SeqCst);
1405 return Ok(VmValue::Int(prev));
1406 }
1407 }
1408 Ok(VmValue::Nil)
1409 });
1410
1411 vm.register_builtin("atomic_cas", |args, _out| {
1412 if args.len() >= 3 {
1413 if let (VmValue::Atomic(a), Some(expected), Some(new_val)) =
1414 (&args[0], args[1].as_int(), args[2].as_int())
1415 {
1416 let result =
1417 a.value
1418 .compare_exchange(expected, new_val, Ordering::SeqCst, Ordering::SeqCst);
1419 return Ok(VmValue::Bool(result.is_ok()));
1420 }
1421 }
1422 Ok(VmValue::Bool(false))
1423 });
1424
1425 vm.register_async_builtin("sleep", |args| async move {
1431 let ms = match args.first() {
1432 Some(VmValue::Duration(ms)) => *ms,
1433 Some(VmValue::Int(n)) => *n as u64,
1434 _ => 0,
1435 };
1436 if ms > 0 {
1437 tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
1438 }
1439 Ok(VmValue::Nil)
1440 });
1441
1442 vm.register_async_builtin("send", |args| async move {
1444 if args.len() < 2 {
1445 return Err(VmError::Thrown(VmValue::String(Rc::from(
1446 "send: requires channel and value",
1447 ))));
1448 }
1449 if let VmValue::Channel(ch) = &args[0] {
1450 if ch.closed.load(Ordering::SeqCst) {
1451 return Ok(VmValue::Bool(false));
1452 }
1453 let val = args[1].clone();
1454 match ch.sender.send(val).await {
1455 Ok(()) => Ok(VmValue::Bool(true)),
1456 Err(_) => Ok(VmValue::Bool(false)),
1457 }
1458 } else {
1459 Err(VmError::Thrown(VmValue::String(Rc::from(
1460 "send: first argument must be a channel",
1461 ))))
1462 }
1463 });
1464
1465 vm.register_async_builtin("receive", |args| async move {
1467 if args.is_empty() {
1468 return Err(VmError::Thrown(VmValue::String(Rc::from(
1469 "receive: requires a channel",
1470 ))));
1471 }
1472 if let VmValue::Channel(ch) = &args[0] {
1473 if ch.closed.load(Ordering::SeqCst) {
1474 let mut rx = ch.receiver.lock().await;
1475 return match rx.try_recv() {
1476 Ok(val) => Ok(val),
1477 Err(_) => Ok(VmValue::Nil),
1478 };
1479 }
1480 let mut rx = ch.receiver.lock().await;
1481 match rx.recv().await {
1482 Some(val) => Ok(val),
1483 None => Ok(VmValue::Nil),
1484 }
1485 } else {
1486 Err(VmError::Thrown(VmValue::String(Rc::from(
1487 "receive: first argument must be a channel",
1488 ))))
1489 }
1490 });
1491
1492 vm.register_async_builtin("select", |args| async move {
1494 if args.is_empty() {
1495 return Err(VmError::Thrown(VmValue::String(Rc::from(
1496 "select: requires at least one channel",
1497 ))));
1498 }
1499
1500 let mut channels: Vec<&VmChannelHandle> = Vec::new();
1501 for arg in &args {
1502 if let VmValue::Channel(ch) = arg {
1503 channels.push(ch);
1504 } else {
1505 return Err(VmError::Thrown(VmValue::String(Rc::from(
1506 "select: all arguments must be channels",
1507 ))));
1508 }
1509 }
1510
1511 loop {
1512 let mut all_closed = true;
1513 for (i, ch) in channels.iter().enumerate() {
1514 if let Ok(mut rx) = ch.receiver.try_lock() {
1515 match rx.try_recv() {
1516 Ok(val) => {
1517 let mut result = BTreeMap::new();
1518 result.insert("index".to_string(), VmValue::Int(i as i64));
1519 result.insert("value".to_string(), val);
1520 result.insert(
1521 "channel".to_string(),
1522 VmValue::String(Rc::from(ch.name.as_str())),
1523 );
1524 return Ok(VmValue::Dict(Rc::new(result)));
1525 }
1526 Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {
1527 all_closed = false;
1528 }
1529 Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {}
1530 }
1531 }
1532 }
1533 if all_closed {
1534 return Ok(VmValue::Nil);
1535 }
1536 tokio::time::sleep(tokio::time::Duration::from_millis(1)).await;
1537 }
1538 });
1539
1540 vm.register_builtin("json_validate", |args, _out| {
1545 if args.len() < 2 {
1546 return Err(VmError::Thrown(VmValue::String(Rc::from(
1547 "json_validate requires 2 arguments: data and schema",
1548 ))));
1549 }
1550 let data = &args[0];
1551 let schema = &args[1];
1552 let schema_dict = match schema.as_dict() {
1553 Some(d) => d,
1554 None => {
1555 return Err(VmError::Thrown(VmValue::String(Rc::from(
1556 "json_validate: schema must be a dict",
1557 ))));
1558 }
1559 };
1560 let mut errors = Vec::new();
1561 validate_value(data, schema_dict, "", &mut errors);
1562 if errors.is_empty() {
1563 Ok(VmValue::Bool(true))
1564 } else {
1565 Err(VmError::Thrown(VmValue::String(Rc::from(
1566 errors.join("; "),
1567 ))))
1568 }
1569 });
1570
1571 vm.register_builtin("json_extract", |args, _out| {
1572 if args.is_empty() {
1573 return Err(VmError::Thrown(VmValue::String(Rc::from(
1574 "json_extract requires at least 1 argument: text",
1575 ))));
1576 }
1577 let text = args[0].display();
1578 let key = args.get(1).map(|a| a.display());
1579
1580 let json_str = extract_json_from_text(&text);
1582 let parsed = match serde_json::from_str::<serde_json::Value>(&json_str) {
1583 Ok(jv) => json_to_vm_value(&jv),
1584 Err(e) => {
1585 return Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1586 "json_extract: failed to parse JSON: {e}"
1587 )))));
1588 }
1589 };
1590
1591 match key {
1592 Some(k) => match &parsed {
1593 VmValue::Dict(map) => match map.get(&k) {
1594 Some(val) => Ok(val.clone()),
1595 None => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
1596 "json_extract: key '{}' not found",
1597 k
1598 ))))),
1599 },
1600 _ => Err(VmError::Thrown(VmValue::String(Rc::from(
1601 "json_extract: parsed value is not a dict, cannot extract key",
1602 )))),
1603 },
1604 None => Ok(parsed),
1605 }
1606 });
1607
1608 register_http_builtins(vm);
1613 register_llm_builtins(vm);
1614 register_mcp_builtins(vm);
1615}
1616
1617pub(crate) fn escape_json_string_vm(s: &str) -> String {
1622 let mut out = String::with_capacity(s.len() + 2);
1623 out.push('"');
1624 for ch in s.chars() {
1625 match ch {
1626 '"' => out.push_str("\\\""),
1627 '\\' => out.push_str("\\\\"),
1628 '\n' => out.push_str("\\n"),
1629 '\r' => out.push_str("\\r"),
1630 '\t' => out.push_str("\\t"),
1631 c if c.is_control() => {
1632 out.push_str(&format!("\\u{:04x}", c as u32));
1633 }
1634 c => out.push(c),
1635 }
1636 }
1637 out.push('"');
1638 out
1639}
1640
1641pub(crate) fn vm_value_to_json(val: &VmValue) -> String {
1642 match val {
1643 VmValue::String(s) => escape_json_string_vm(s),
1644 VmValue::Int(n) => n.to_string(),
1645 VmValue::Float(n) => n.to_string(),
1646 VmValue::Bool(b) => b.to_string(),
1647 VmValue::Nil => "null".to_string(),
1648 VmValue::List(items) => {
1649 let inner: Vec<String> = items.iter().map(vm_value_to_json).collect();
1650 format!("[{}]", inner.join(","))
1651 }
1652 VmValue::Dict(map) => {
1653 let inner: Vec<String> = map
1654 .iter()
1655 .map(|(k, v)| format!("{}:{}", escape_json_string_vm(k), vm_value_to_json(v)))
1656 .collect();
1657 format!("{{{}}}", inner.join(","))
1658 }
1659 _ => "null".to_string(),
1660 }
1661}
1662
1663pub(crate) fn json_to_vm_value(jv: &serde_json::Value) -> VmValue {
1664 match jv {
1665 serde_json::Value::Null => VmValue::Nil,
1666 serde_json::Value::Bool(b) => VmValue::Bool(*b),
1667 serde_json::Value::Number(n) => {
1668 if let Some(i) = n.as_i64() {
1669 VmValue::Int(i)
1670 } else {
1671 VmValue::Float(n.as_f64().unwrap_or(0.0))
1672 }
1673 }
1674 serde_json::Value::String(s) => VmValue::String(Rc::from(s.as_str())),
1675 serde_json::Value::Array(arr) => {
1676 VmValue::List(Rc::new(arr.iter().map(json_to_vm_value).collect()))
1677 }
1678 serde_json::Value::Object(map) => {
1679 let mut m = BTreeMap::new();
1680 for (k, v) in map {
1681 m.insert(k.clone(), json_to_vm_value(v));
1682 }
1683 VmValue::Dict(Rc::new(m))
1684 }
1685 }
1686}
1687
1688fn validate_value(
1693 value: &VmValue,
1694 schema: &BTreeMap<String, VmValue>,
1695 path: &str,
1696 errors: &mut Vec<String>,
1697) {
1698 if let Some(VmValue::String(expected_type)) = schema.get("type") {
1700 let actual_type = value.type_name();
1701 let type_str: &str = expected_type;
1702 if type_str != "any" && actual_type != type_str {
1703 let location = if path.is_empty() {
1704 "root".to_string()
1705 } else {
1706 path.to_string()
1707 };
1708 errors.push(format!(
1709 "at {}: expected type '{}', got '{}'",
1710 location, type_str, actual_type
1711 ));
1712 return; }
1714 }
1715
1716 if let Some(VmValue::List(required_keys)) = schema.get("required") {
1718 if let VmValue::Dict(map) = value {
1719 for key_val in required_keys.iter() {
1720 let key = key_val.display();
1721 if !map.contains_key(&key) {
1722 let location = if path.is_empty() {
1723 "root".to_string()
1724 } else {
1725 path.to_string()
1726 };
1727 errors.push(format!("at {}: missing required key '{}'", location, key));
1728 }
1729 }
1730 }
1731 }
1732
1733 if let Some(VmValue::Dict(prop_schemas)) = schema.get("properties") {
1735 if let VmValue::Dict(map) = value {
1736 for (key, prop_schema) in prop_schemas.iter() {
1737 if let Some(prop_value) = map.get(key) {
1738 if let Some(prop_schema_dict) = prop_schema.as_dict() {
1739 let child_path = if path.is_empty() {
1740 key.clone()
1741 } else {
1742 format!("{}.{}", path, key)
1743 };
1744 validate_value(prop_value, prop_schema_dict, &child_path, errors);
1745 }
1746 }
1747 }
1748 }
1749 }
1750
1751 if let Some(VmValue::Dict(item_schema)) = schema.get("items") {
1753 if let VmValue::List(items) = value {
1754 for (i, item) in items.iter().enumerate() {
1755 let child_path = if path.is_empty() {
1756 format!("[{}]", i)
1757 } else {
1758 format!("{}[{}]", path, i)
1759 };
1760 validate_value(item, item_schema, &child_path, errors);
1761 }
1762 }
1763 }
1764}
1765
1766fn extract_json_from_text(text: &str) -> String {
1771 let trimmed = text.trim();
1772
1773 if let Some(start) = trimmed.find("```") {
1775 let after_backticks = &trimmed[start + 3..];
1776 let content_start = if let Some(nl) = after_backticks.find('\n') {
1778 nl + 1
1779 } else {
1780 0
1781 };
1782 let content = &after_backticks[content_start..];
1783 if let Some(end) = content.find("```") {
1784 return content[..end].trim().to_string();
1785 }
1786 }
1787
1788 if let Some(obj_start) = trimmed.find('{') {
1791 if let Some(obj_end) = trimmed.rfind('}') {
1792 if obj_end > obj_start {
1793 return trimmed[obj_start..=obj_end].to_string();
1794 }
1795 }
1796 }
1797 if let Some(arr_start) = trimmed.find('[') {
1798 if let Some(arr_end) = trimmed.rfind(']') {
1799 if arr_end > arr_start {
1800 return trimmed[arr_start..=arr_end].to_string();
1801 }
1802 }
1803 }
1804
1805 trimmed.to_string()
1807}
1808
1809fn vm_output_to_value(output: std::process::Output) -> VmValue {
1814 let mut result = BTreeMap::new();
1815 result.insert(
1816 "stdout".to_string(),
1817 VmValue::String(Rc::from(
1818 String::from_utf8_lossy(&output.stdout).to_string().as_str(),
1819 )),
1820 );
1821 result.insert(
1822 "stderr".to_string(),
1823 VmValue::String(Rc::from(
1824 String::from_utf8_lossy(&output.stderr).to_string().as_str(),
1825 )),
1826 );
1827 result.insert(
1828 "status".to_string(),
1829 VmValue::Int(output.status.code().unwrap_or(-1) as i64),
1830 );
1831 result.insert(
1832 "success".to_string(),
1833 VmValue::Bool(output.status.success()),
1834 );
1835 VmValue::Dict(Rc::new(result))
1836}
1837
1838fn vm_civil_from_timestamp(total_secs: u64) -> (i64, i64, i64, i64, i64, i64, i64) {
1843 let days = total_secs / 86400;
1844 let time_of_day = total_secs % 86400;
1845 let hour = (time_of_day / 3600) as i64;
1846 let minute = ((time_of_day % 3600) / 60) as i64;
1847 let second = (time_of_day % 60) as i64;
1848
1849 let z = days as i64 + 719468;
1850 let era = if z >= 0 { z } else { z - 146096 } / 146097;
1851 let doe = (z - era * 146097) as u64;
1852 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1853 let y = yoe as i64 + era * 400;
1854 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1855 let mp = (5 * doy + 2) / 153;
1856 let d = (doy - (153 * mp + 2) / 5 + 1) as i64;
1857 let m = if mp < 10 { mp + 3 } else { mp - 9 } as i64;
1858 let y = if m <= 2 { y + 1 } else { y };
1859 let dow = ((days + 4) % 7) as i64;
1860
1861 (y, m, d, hour, minute, second, dow)
1862}
1863
1864pub(crate) static VM_MIN_LOG_LEVEL: AtomicU8 = AtomicU8::new(0);
1869
1870#[derive(Clone)]
1871pub(crate) struct VmTraceContext {
1872 pub(crate) trace_id: String,
1873 pub(crate) span_id: String,
1874}
1875
1876thread_local! {
1877 pub(crate) static VM_TRACE_STACK: std::cell::RefCell<Vec<VmTraceContext>> = const { std::cell::RefCell::new(Vec::new()) };
1878}
1879
1880fn vm_level_to_u8(level: &str) -> Option<u8> {
1881 match level {
1882 "debug" => Some(0),
1883 "info" => Some(1),
1884 "warn" => Some(2),
1885 "error" => Some(3),
1886 _ => None,
1887 }
1888}
1889
1890fn vm_format_timestamp_utc() -> String {
1891 let now = std::time::SystemTime::now()
1892 .duration_since(std::time::UNIX_EPOCH)
1893 .unwrap_or_default();
1894 let total_secs = now.as_secs();
1895 let millis = now.subsec_millis();
1896
1897 let days = total_secs / 86400;
1898 let time_of_day = total_secs % 86400;
1899 let hour = time_of_day / 3600;
1900 let minute = (time_of_day % 3600) / 60;
1901 let second = time_of_day % 60;
1902
1903 let z = days as i64 + 719468;
1904 let era = if z >= 0 { z } else { z - 146096 } / 146097;
1905 let doe = (z - era * 146097) as u64;
1906 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
1907 let y = yoe as i64 + era * 400;
1908 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
1909 let mp = (5 * doy + 2) / 153;
1910 let d = doy - (153 * mp + 2) / 5 + 1;
1911 let m = if mp < 10 { mp + 3 } else { mp - 9 };
1912 let y = if m <= 2 { y + 1 } else { y };
1913
1914 format!("{y:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
1915}
1916
1917pub(crate) fn vm_escape_json_str(s: &str) -> String {
1918 let mut out = String::with_capacity(s.len());
1919 for ch in s.chars() {
1920 match ch {
1921 '"' => out.push_str("\\\""),
1922 '\\' => out.push_str("\\\\"),
1923 '\n' => out.push_str("\\n"),
1924 '\r' => out.push_str("\\r"),
1925 '\t' => out.push_str("\\t"),
1926 c if c.is_control() => {
1927 out.push_str(&format!("\\u{:04x}", c as u32));
1928 }
1929 c => out.push(c),
1930 }
1931 }
1932 out
1933}
1934
1935fn vm_escape_json_str_quoted(s: &str) -> String {
1936 let mut out = String::with_capacity(s.len() + 2);
1937 out.push('"');
1938 out.push_str(&vm_escape_json_str(s));
1939 out.push('"');
1940 out
1941}
1942
1943fn vm_value_to_json_fragment(val: &VmValue) -> String {
1944 match val {
1945 VmValue::String(s) => vm_escape_json_str_quoted(s),
1946 VmValue::Int(n) => n.to_string(),
1947 VmValue::Float(n) => {
1948 if n.is_finite() {
1949 n.to_string()
1950 } else {
1951 "null".to_string()
1952 }
1953 }
1954 VmValue::Bool(b) => b.to_string(),
1955 VmValue::Nil => "null".to_string(),
1956 _ => vm_escape_json_str_quoted(&val.display()),
1957 }
1958}
1959
1960fn vm_build_log_line(level: &str, msg: &str, fields: Option<&BTreeMap<String, VmValue>>) -> String {
1961 let ts = vm_format_timestamp_utc();
1962 let mut parts: Vec<String> = Vec::new();
1963 parts.push(format!("\"ts\":{}", vm_escape_json_str_quoted(&ts)));
1964 parts.push(format!("\"level\":{}", vm_escape_json_str_quoted(level)));
1965 parts.push(format!("\"msg\":{}", vm_escape_json_str_quoted(msg)));
1966
1967 VM_TRACE_STACK.with(|stack| {
1968 if let Some(trace) = stack.borrow().last() {
1969 parts.push(format!(
1970 "\"trace_id\":{}",
1971 vm_escape_json_str_quoted(&trace.trace_id)
1972 ));
1973 parts.push(format!(
1974 "\"span_id\":{}",
1975 vm_escape_json_str_quoted(&trace.span_id)
1976 ));
1977 }
1978 });
1979
1980 if let Some(dict) = fields {
1981 for (k, v) in dict {
1982 parts.push(format!(
1983 "{}:{}",
1984 vm_escape_json_str_quoted(k),
1985 vm_value_to_json_fragment(v)
1986 ));
1987 }
1988 }
1989
1990 format!("{{{}}}\n", parts.join(","))
1991}
1992
1993fn vm_write_log(level: &str, level_num: u8, args: &[VmValue], out: &mut String) {
1994 if level_num < VM_MIN_LOG_LEVEL.load(Ordering::Relaxed) {
1995 return;
1996 }
1997 let msg = args.first().map(|a| a.display()).unwrap_or_default();
1998 let fields = args.get(1).and_then(|v| {
1999 if let VmValue::Dict(d) = v {
2000 Some(&**d)
2001 } else {
2002 None
2003 }
2004 });
2005 let line = vm_build_log_line(level, &msg, fields);
2006 out.push_str(&line);
2007}
2008
2009fn vm_validate_registry(name: &str, dict: &BTreeMap<String, VmValue>) -> Result<(), VmError> {
2014 match dict.get("_type") {
2015 Some(VmValue::String(t)) if &**t == "tool_registry" => Ok(()),
2016 _ => Err(VmError::Thrown(VmValue::String(Rc::from(format!(
2017 "{name}: argument must be a tool registry (created with tool_registry())"
2018 ))))),
2019 }
2020}
2021
2022fn vm_get_tools(dict: &BTreeMap<String, VmValue>) -> &[VmValue] {
2023 match dict.get("tools") {
2024 Some(VmValue::List(list)) => list,
2025 _ => &[],
2026 }
2027}
2028
2029fn vm_format_parameters(params: Option<&VmValue>) -> String {
2030 match params {
2031 Some(VmValue::Dict(map)) if !map.is_empty() => {
2032 let mut pairs: Vec<(String, String)> =
2033 map.iter().map(|(k, v)| (k.clone(), v.display())).collect();
2034 pairs.sort_by(|a, b| a.0.cmp(&b.0));
2035 pairs
2036 .iter()
2037 .map(|(k, v)| format!("{k}: {v}"))
2038 .collect::<Vec<_>>()
2039 .join(", ")
2040 }
2041 _ => String::new(),
2042 }
2043}
2044
2045fn vm_build_empty_schema() -> BTreeMap<String, VmValue> {
2046 let mut schema = BTreeMap::new();
2047 schema.insert(
2048 "schema_version".to_string(),
2049 VmValue::String(Rc::from("harn-tools/1.0")),
2050 );
2051 schema.insert("tools".to_string(), VmValue::List(Rc::new(Vec::new())));
2052 schema
2053}
2054
2055fn vm_build_input_schema(
2056 params: Option<&VmValue>,
2057 components: Option<&BTreeMap<String, VmValue>>,
2058) -> VmValue {
2059 let mut schema = BTreeMap::new();
2060 schema.insert("type".to_string(), VmValue::String(Rc::from("object")));
2061
2062 let params_map = match params {
2063 Some(VmValue::Dict(map)) if !map.is_empty() => map,
2064 _ => {
2065 schema.insert(
2066 "properties".to_string(),
2067 VmValue::Dict(Rc::new(BTreeMap::new())),
2068 );
2069 return VmValue::Dict(Rc::new(schema));
2070 }
2071 };
2072
2073 let mut properties = BTreeMap::new();
2074 let mut required = Vec::new();
2075
2076 for (key, val) in params_map.iter() {
2077 let prop = vm_resolve_param_type(val, components);
2078 properties.insert(key.clone(), prop);
2079 required.push(VmValue::String(Rc::from(key.as_str())));
2080 }
2081
2082 schema.insert("properties".to_string(), VmValue::Dict(Rc::new(properties)));
2083 if !required.is_empty() {
2084 required.sort_by_key(|a| a.display());
2085 schema.insert("required".to_string(), VmValue::List(Rc::new(required)));
2086 }
2087
2088 VmValue::Dict(Rc::new(schema))
2089}
2090
2091fn vm_resolve_param_type(val: &VmValue, components: Option<&BTreeMap<String, VmValue>>) -> VmValue {
2092 match val {
2093 VmValue::String(type_name) => {
2094 let json_type = vm_harn_type_to_json_schema(type_name);
2095 let mut prop = BTreeMap::new();
2096 prop.insert("type".to_string(), VmValue::String(Rc::from(json_type)));
2097 VmValue::Dict(Rc::new(prop))
2098 }
2099 VmValue::Dict(map) => {
2100 if let Some(VmValue::String(ref_name)) = map.get("$ref") {
2101 if let Some(comps) = components {
2102 if let Some(resolved) = comps.get(&**ref_name) {
2103 return resolved.clone();
2104 }
2105 }
2106 let mut prop = BTreeMap::new();
2107 prop.insert(
2108 "$ref".to_string(),
2109 VmValue::String(Rc::from(
2110 format!("#/components/schemas/{ref_name}").as_str(),
2111 )),
2112 );
2113 VmValue::Dict(Rc::new(prop))
2114 } else {
2115 VmValue::Dict(Rc::new((**map).clone()))
2116 }
2117 }
2118 _ => {
2119 let mut prop = BTreeMap::new();
2120 prop.insert("type".to_string(), VmValue::String(Rc::from("string")));
2121 VmValue::Dict(Rc::new(prop))
2122 }
2123 }
2124}
2125
2126fn vm_harn_type_to_json_schema(harn_type: &str) -> &str {
2127 match harn_type {
2128 "int" => "integer",
2129 "float" => "number",
2130 "bool" | "boolean" => "boolean",
2131 "list" | "array" => "array",
2132 "dict" | "object" => "object",
2133 _ => "string",
2134 }
2135}