Skip to main content

harn_vm/
stdlib.rs

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