Skip to main content

harn_vm/
stdlib.rs

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