1use std::cell::RefCell;
2use std::collections::HashSet;
3use std::rc::Rc;
4
5use sema_core::{
6 intern, resolve, Env, EvalContext, Macro, MultiMethod, NativeFn, SemaError, Spur, Thunk, Value,
7 ValueView,
8};
9
10use crate::special_forms;
11
12pub enum Trampoline {
14 Value(Value),
15 Eval(Value, Env),
16}
17
18pub type EvalResult = Result<Value, SemaError>;
19
20pub fn create_module_env(env: &Env) -> Env {
22 let mut current = env.clone();
24 loop {
25 let parent = current.parent.clone();
26 match parent {
27 Some(p) => current = (*p).clone(),
28 None => break,
29 }
30 }
31 Env::with_parent(Rc::new(current))
32}
33
34fn collect_native_names(env: &Env) -> HashSet<Spur> {
37 env.all_names()
38 .into_iter()
39 .filter(|&spur| env.get(spur).is_some_and(|v| v.is_native_fn()))
40 .collect()
41}
42
43pub struct Interpreter {
45 pub global_env: Rc<Env>,
46 pub ctx: EvalContext,
47}
48
49impl Default for Interpreter {
50 fn default() -> Self {
51 Self::new()
52 }
53}
54
55impl Interpreter {
56 pub fn new() -> Self {
57 let env = Env::new();
58 let ctx = EvalContext::new();
59 sema_core::set_eval_callback(&ctx, eval_value_vm);
61 sema_core::set_call_callback(&ctx, call_value);
62 sema_stdlib::register_stdlib(&env, &sema_core::Sandbox::allow_all());
64 #[cfg(not(target_arch = "wasm32"))]
66 {
67 sema_llm::builtins::reset_runtime_state();
68 sema_llm::builtins::register_llm_builtins(&env, &sema_core::Sandbox::allow_all());
69 }
70 let global_env = Rc::new(env);
71 register_vm_delegates(&global_env);
72 load_prelude(&ctx, &global_env);
73 Interpreter { global_env, ctx }
74 }
75
76 pub fn new_with_sandbox(sandbox: &sema_core::Sandbox) -> Self {
77 let env = Env::new();
78 let ctx = EvalContext::new_with_sandbox(sandbox.clone());
79 sema_core::set_eval_callback(&ctx, eval_value_vm);
80 sema_core::set_call_callback(&ctx, call_value);
81 sema_stdlib::register_stdlib(&env, sandbox);
82 #[cfg(not(target_arch = "wasm32"))]
83 {
84 sema_llm::builtins::reset_runtime_state();
85 sema_llm::builtins::register_llm_builtins(&env, sandbox);
86 }
87 let global_env = Rc::new(env);
88 register_vm_delegates(&global_env);
89 load_prelude(&ctx, &global_env);
90 Interpreter { global_env, ctx }
91 }
92
93 pub fn eval(&self, expr: &Value) -> EvalResult {
101 self.eval_in_global(expr)
102 }
103
104 pub fn eval_str(&self, input: &str) -> EvalResult {
106 self.eval_str_in_global(input)
107 }
108
109 pub fn eval_in_global(&self, expr: &Value) -> EvalResult {
111 self.run_exprs_on_vm(std::slice::from_ref(expr), &self.global_env)
112 }
113
114 pub fn eval_str_in_global(&self, input: &str) -> EvalResult {
116 let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
117 self.ctx.merge_span_table(spans);
118 if exprs.is_empty() {
119 return Ok(Value::nil());
120 }
121 self.run_exprs_on_vm(&exprs, &self.global_env)
122 }
123
124 pub fn eval_str_compiled(&self, input: &str) -> EvalResult {
126 let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
127 self.ctx.merge_span_table(spans);
128 if exprs.is_empty() {
129 return Ok(Value::nil());
130 }
131 self.run_exprs_on_vm(&exprs, &self.global_env)
132 }
133
134 fn run_exprs_on_vm(&self, exprs: &[Value], globals: &Rc<Env>) -> EvalResult {
138 let mut expanded = Vec::with_capacity(exprs.len());
139 for expr in exprs {
140 expanded.push(expand_for_vm_in(&self.ctx, globals, expr)?);
141 }
142 let known_natives = collect_native_names(globals);
143 let prog = sema_vm::compile_program(&expanded, Some(known_natives))?;
144 let mut vm = sema_vm::VM::new(
145 globals.clone(),
146 prog.functions,
147 &prog.native_table,
148 prog.main_cache_slots,
149 )?;
150 sema_vm::init_scheduler(self.global_env.clone(), prog.native_table.clone());
151 self.ctx.eval_steps.set(0);
154 vm.execute(prog.closure, &self.ctx)
155 }
156
157 pub fn compile_to_bytecode(&self, input: &str) -> Result<sema_vm::CompileResult, SemaError> {
160 let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
161 self.ctx.merge_span_table(spans);
162
163 let mut expanded = Vec::new();
164 for expr in &exprs {
165 let exp = self.expand_for_vm(expr)?;
166 if !exp.is_nil() {
167 expanded.push(exp);
168 }
169 }
170
171 if expanded.is_empty() {
172 expanded.push(Value::nil());
173 }
174
175 let prog = sema_vm::compile_program(&expanded, None)?;
176 Ok(sema_vm::CompileResult::new(
177 prog.closure.func.chunk.clone(),
178 prog.functions.iter().map(|f| (**f).clone()).collect(),
179 ))
180 }
181
182 pub fn expand_for_vm(&self, expr: &Value) -> EvalResult {
185 expand_for_vm_in(&self.ctx, &self.global_env, expr)
186 }
187}
188
189pub fn expand_for_vm_in(ctx: &EvalContext, env: &Env, expr: &Value) -> EvalResult {
196 if let Some(items) = expr.as_list() {
197 if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
198 let name = resolve(s);
199 if name == "defmacro" {
200 register_defmacro(items, env)?;
203 return Ok(Value::nil());
204 }
205 if name == "begin" || name == "progn" {
206 let mut new_items = vec![Value::symbol_from_spur(s)];
207 let mut changed = false;
208 for item in &items[1..] {
209 let expanded = expand_for_vm_in(ctx, env, item)?;
210 if expanded.raw_bits() != item.raw_bits() {
211 changed = true;
212 }
213 new_items.push(expanded);
214 }
215 if !changed {
216 return Ok(expr.clone());
217 }
218 return Ok(Value::list(new_items));
219 }
220 }
221 }
222 expand_macros_in(ctx, env, expr)
223}
224
225fn expand_macros_in(ctx: &EvalContext, env: &Env, expr: &Value) -> EvalResult {
229 if let Some(items) = expr.as_list() {
230 if !items.is_empty() {
231 if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
232 let name = resolve(s);
233 if name == "quote" {
234 return Ok(expr.clone());
235 }
236 if let Some(mac_val) = env.get(s) {
237 if let Some(mac) = mac_val.as_macro_rc() {
238 let expanded = apply_macro_vm(ctx, &mac, &items[1..], env)?;
241 return expand_macros_in(ctx, env, &expanded);
242 }
243 }
244 }
245 let expanded: Vec<Value> = items
246 .iter()
247 .map(|v| expand_macros_in(ctx, env, v))
248 .collect::<Result<_, _>>()?;
249 let changed = expanded
250 .iter()
251 .zip(items.iter())
252 .any(|(a, b)| a.raw_bits() != b.raw_bits());
253 if !changed {
254 return Ok(expr.clone());
255 }
256 return Ok(Value::list(expanded));
257 }
258 }
259 Ok(expr.clone())
260}
261
262pub fn eval_module_body_vm(
274 ctx: &EvalContext,
275 env: &Env,
276 exprs: &[Value],
277 span_map: &sema_core::SpanMap,
278 source_file: Option<std::path::PathBuf>,
279) -> EvalResult {
280 let mut result = Value::nil();
281 for expr in exprs {
282 let expanded = expand_for_vm_in(ctx, env, expr)?;
283 if expanded.is_nil() {
286 continue;
287 }
288 let prog = sema_vm::compile_program_with_spans(
289 std::slice::from_ref(&expanded),
290 span_map,
291 source_file.clone(),
292 )?;
293 let globals = Rc::new(env.clone());
294 let mut vm = sema_vm::VM::new(
295 globals,
296 prog.functions,
297 &prog.native_table,
298 prog.main_cache_slots,
299 )?;
300 result = vm.execute(prog.closure, ctx)?;
301 }
302 env.bump_version();
307 Ok(result)
308}
309
310pub fn eval_value_vm(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
317 let env_rc = Rc::new(env.clone());
318 let expanded = expand_for_vm_in(ctx, &env_rc, expr)?;
319 if expanded.is_nil() {
320 return Ok(Value::nil());
321 }
322 let prog = sema_vm::compile_program(std::slice::from_ref(&expanded), None)?;
323 let mut vm = sema_vm::VM::new(env_rc, prog.functions, &[], prog.main_cache_slots)?;
324 vm.execute(prog.closure, ctx)
325}
326
327pub fn call_value(ctx: &EvalContext, func: &Value, args: &[Value]) -> EvalResult {
334 match func.view() {
335 ValueView::NativeFn(native) => (native.func)(ctx, args),
336 ValueView::Lambda(_) => {
337 Err(SemaError::eval(
341 "internal: raw lambda value reached call_value (VM closures are native-fn-wrapped)"
342 .to_string(),
343 ))
344 }
345 ValueView::Keyword(spur) => {
346 if args.len() != 1 {
347 let name = resolve(spur);
348 return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
349 }
350 let key = Value::keyword_from_spur(spur);
351 match args[0].view() {
352 ValueView::Map(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
353 ValueView::HashMap(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
354 _ => Err(SemaError::type_error_with_value(
355 "map",
356 args[0].type_name(),
357 &args[0],
358 )),
359 }
360 }
361 ValueView::MultiMethod(mm) => call_multimethod(ctx, &mm, args),
362 _ => Err(
363 SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
364 .with_hint("expected a function, lambda, or keyword"),
365 ),
366 }
367}
368
369fn call_multimethod(ctx: &EvalContext, mm: &Rc<MultiMethod>, args: &[Value]) -> EvalResult {
371 let dispatch_val = call_value(ctx, &mm.dispatch_fn, args)?;
372 let methods = mm.methods.borrow();
373 if let Some(handler) = methods.get(&dispatch_val) {
374 let handler = handler.clone();
375 drop(methods);
376 call_value(ctx, &handler, args)
377 } else {
378 drop(methods);
379 let default = mm.default.borrow().clone();
380 if let Some(handler) = default {
381 call_value(ctx, &handler, args)
382 } else {
383 Err(SemaError::eval(format!(
384 "no method in multimethod '{}' for dispatch value: {}",
385 resolve(mm.name),
386 dispatch_val
387 ))
388 .with_hint("add a (defmethod name :default handler) to handle unmatched values"))
389 }
390 }
391}
392
393pub fn apply_macro_vm(
411 ctx: &EvalContext,
412 mac: &sema_core::Macro,
413 args: &[Value],
414 caller_env: &Env,
415) -> Result<Value, SemaError> {
416 let env = Rc::new(Env::with_parent(Rc::new(caller_env.clone())));
417
418 if let Some(rest) = mac.rest_param {
420 if args.len() < mac.params.len() {
421 return Err(SemaError::arity(
422 resolve(mac.name),
423 format!("{}+", mac.params.len()),
424 args.len(),
425 ));
426 }
427 for (param, arg) in mac.params.iter().zip(args.iter()) {
428 env.set(*param, arg.clone());
429 }
430 env.set(rest, Value::list(args[mac.params.len()..].to_vec()));
431 } else {
432 if args.len() != mac.params.len() {
433 return Err(SemaError::arity(
434 resolve(mac.name),
435 mac.params.len().to_string(),
436 args.len(),
437 ));
438 }
439 for (param, arg) in mac.params.iter().zip(args.iter()) {
440 env.set(*param, arg.clone());
441 }
442 }
443
444 let mut result = Value::nil();
454 for expr in &mac.body {
455 let prog = sema_vm::compile_program(std::slice::from_ref(expr), None)?;
456 let mut vm = sema_vm::VM::new(env.clone(), prog.functions, &[], prog.main_cache_slots)?;
457 result = vm.execute(prog.closure, ctx)?;
458 }
459 Ok(result)
460}
461
462fn register_defmacro(items: &[Value], env: &Env) -> Result<(), SemaError> {
466 let args = &items[1..];
468 if args.len() < 3 {
469 return Err(SemaError::arity("defmacro", "3+", args.len()));
470 }
471 let name_spur = args[0]
472 .as_symbol_spur()
473 .ok_or_else(|| SemaError::eval("defmacro: name must be a symbol"))?;
474 let param_list = args[1]
475 .as_list()
476 .ok_or_else(|| SemaError::eval("defmacro: params must be a list"))?;
477 let param_names: Vec<sema_core::Spur> = param_list
478 .iter()
479 .map(|v| {
480 v.as_symbol_spur()
481 .ok_or_else(|| SemaError::eval("defmacro: parameter must be a symbol"))
482 })
483 .collect::<Result<_, _>>()?;
484 let (params, rest_param) = special_forms::parse_params(¶m_names);
485 let body = args[2..].to_vec();
486 env.set(
487 name_spur,
488 Value::macro_val(Macro {
489 params,
490 rest_param,
491 body,
492 name: name_spur,
493 }),
494 );
495 Ok(())
496}
497
498pub fn load_prelude(ctx: &EvalContext, env: &Rc<Env>) {
502 let exprs = sema_reader::read_many(crate::prelude::PRELUDE)
503 .unwrap_or_else(|e| panic!("internal: prelude failed to parse: {e}"));
504 for expr in &exprs {
508 expand_for_vm_in(ctx, env, expr)
509 .unwrap_or_else(|e| panic!("internal: prelude failed to load: {e}"));
510 }
511}
512
513pub fn register_vm_delegates(env: &Rc<Env>) {
514 let eval_env = env.clone();
519 env.set(
520 intern("__vm-eval"),
521 Value::native_fn(NativeFn::with_ctx("__vm-eval", move |ctx, args| {
522 if args.len() != 1 {
523 return Err(SemaError::arity("eval", "1", args.len()));
524 }
525 let expanded = expand_for_vm_in(ctx, &eval_env, &args[0])?;
526 if expanded.is_nil() {
528 return Ok(Value::nil());
529 }
530 let prog = sema_vm::compile_program(std::slice::from_ref(&expanded), None)?;
531 let mut vm =
532 sema_vm::VM::new(eval_env.clone(), prog.functions, &[], prog.main_cache_slots)?;
533 vm.execute(prog.closure, ctx)
534 })),
535 );
536
537 env.set(
543 intern("__vm-module-exports"),
544 Value::native_fn(NativeFn::with_ctx(
545 "__vm-module-exports",
546 move |ctx, args| {
547 if args.len() != 1 {
548 return Err(SemaError::arity("module-exports", "1", args.len()));
549 }
550 let names: Vec<String> = match args[0].as_list() {
551 Some(items) => items
552 .iter()
553 .map(|v| {
554 v.as_symbol().map(|s| s.to_string()).ok_or_else(|| {
555 SemaError::eval("module: export names must be symbols")
556 })
557 })
558 .collect::<Result<_, _>>()?,
559 None => return Err(SemaError::type_error("list", args[0].type_name())),
560 };
561 ctx.set_module_exports(names);
562 Ok(Value::nil())
563 },
564 )),
565 );
566
567 let load_env = env.clone();
572 env.set(
573 intern("__vm-load"),
574 Value::native_fn(NativeFn::with_ctx("__vm-load", move |ctx, args| {
575 if args.len() != 1 {
576 return Err(SemaError::arity("load", "1", args.len()));
577 }
578 let target = sema_vm::current_vm_globals().unwrap_or_else(|| load_env.clone());
582 match special_forms::eval_load(std::slice::from_ref(&args[0]), &target, ctx)? {
583 Trampoline::Value(v) => Ok(v),
584 Trampoline::Eval(..) => Ok(Value::nil()),
585 }
586 })),
587 );
588
589 let import_env = env.clone();
594 env.set(
595 intern("__vm-import"),
596 Value::native_fn(NativeFn::with_ctx("__vm-import", move |ctx, args| {
597 if args.len() != 2 {
598 return Err(SemaError::arity("import", "2", args.len()));
599 }
600 ctx.sandbox.check(sema_core::Caps::FS_READ, "import")?;
601 let mut imp_args = vec![args[0].clone()];
602 if let Some(items) = args[1].as_list() {
603 imp_args.extend(items.iter().cloned());
604 }
605 let target = sema_vm::current_vm_globals().unwrap_or_else(|| import_env.clone());
610 match special_forms::eval_import(&imp_args, &target, ctx)? {
611 Trampoline::Value(v) => Ok(v),
612 Trampoline::Eval(..) => Ok(Value::nil()),
613 }
614 })),
615 );
616
617 let macro_env = env.clone();
619 env.set(
620 intern("__vm-defmacro"),
621 Value::native_fn(NativeFn::simple("__vm-defmacro", move |args| {
622 if args.len() != 4 {
623 return Err(SemaError::arity("defmacro", "4", args.len()));
624 }
625 let name = match args[0].as_symbol_spur() {
626 Some(s) => s,
627 None => return Err(SemaError::type_error("symbol", args[0].type_name())),
628 };
629 let params = match args[1].as_list() {
630 Some(items) => items
631 .iter()
632 .map(|v| match v.as_symbol_spur() {
633 Some(s) => Ok(s),
634 None => Err(SemaError::type_error("symbol", v.type_name())),
635 })
636 .collect::<Result<Vec<_>, _>>()?,
637 None => return Err(SemaError::type_error("list", args[1].type_name())),
638 };
639 let rest_param = if let Some(s) = args[2].as_symbol_spur() {
640 Some(s)
641 } else if args[2].is_nil() {
642 None
643 } else {
644 return Err(SemaError::type_error("symbol or nil", args[2].type_name()));
645 };
646 let body = vec![args[3].clone()];
647 macro_env.set(
648 name,
649 Value::macro_val(Macro {
650 params,
651 rest_param,
652 body,
653 name,
654 }),
655 );
656 Ok(Value::nil())
657 })),
658 );
659
660 let dmf_env = env.clone();
665 env.set(
666 intern("__vm-defmacro-form"),
667 Value::native_fn(NativeFn::simple("__vm-defmacro-form", move |args| {
668 if args.len() != 1 {
669 return Err(SemaError::arity("defmacro-form", "1", args.len()));
670 }
671 let items = args[0]
672 .as_list()
673 .ok_or_else(|| SemaError::type_error("list", args[0].type_name()))?;
674 register_defmacro(items, &dmf_env)?;
675 Ok(Value::nil())
676 })),
677 );
678
679 let drt_env = env.clone();
681 env.set(
682 intern("__vm-define-record-type"),
683 Value::native_fn(NativeFn::simple("__vm-define-record-type", move |args| {
684 if args.len() != 5 {
685 return Err(SemaError::arity("define-record-type", "5", args.len()));
686 }
687 let mut ctor_form = vec![args[1].clone()];
692 if let Some(fields) = args[3].as_list() {
693 ctor_form.extend(fields.iter().cloned());
694 }
695 let mut dr_args = vec![args[0].clone(), Value::list(ctor_form), args[2].clone()];
696 if let Some(specs) = args[4].as_list() {
697 for spec in specs.iter() {
698 dr_args.push(spec.clone());
699 }
700 }
701 match special_forms::eval_define_record_type(&dr_args, &drt_env)? {
702 Trampoline::Value(v) => Ok(v),
703 Trampoline::Eval(..) => Ok(Value::nil()),
704 }
705 })),
706 );
707
708 env.set(
710 intern("__vm-delay"),
711 Value::native_fn(NativeFn::simple("__vm-delay", |args| {
712 if args.len() != 1 {
713 return Err(SemaError::arity("delay", "1", args.len()));
714 }
715 Ok(Value::thunk(Thunk {
717 body: args[0].clone(),
718 forced: RefCell::new(None),
719 }))
720 })),
721 );
722
723 let force_env = env.clone();
725 env.set(
726 intern("__vm-force"),
727 Value::native_fn(NativeFn::with_ctx("__vm-force", move |ctx, args| {
728 if args.len() != 1 {
729 return Err(SemaError::arity("force", "1", args.len()));
730 }
731 if let Some(thunk) = args[0].as_thunk_rc() {
732 if let Some(val) = thunk.forced.borrow().as_ref() {
733 return Ok(val.clone());
734 }
735 let val = if thunk.body.as_native_fn_rc().is_some()
736 || thunk.body.as_lambda_rc().is_some()
737 {
738 sema_core::call_callback(ctx, &thunk.body, &[])?
739 } else {
740 eval_value_vm(ctx, &thunk.body, &force_env)?
742 };
743 *thunk.forced.borrow_mut() = Some(val.clone());
744 Ok(val)
745 } else {
746 Err(SemaError::type_error("thunk", args[0].type_name())
747 .with_hint("force: argument must be a (delay ...) or promise — non-promise values are an error"))
748 }
749 })),
750 );
751
752 let me_env = env.clone();
754 env.set(
755 intern("__vm-macroexpand"),
756 Value::native_fn(NativeFn::with_ctx("__vm-macroexpand", move |ctx, args| {
757 if args.len() != 1 {
758 return Err(SemaError::arity("macroexpand", "1", args.len()));
759 }
760 if let Some(items) = args[0].as_list() {
761 if !items.is_empty() {
762 if let Some(spur) = items[0].as_symbol_spur() {
763 if let Some(mac_val) = me_env.get(spur) {
764 if let Some(mac) = mac_val.as_macro_rc() {
765 return apply_macro_vm(ctx, &mac, &items[1..], &me_env);
767 }
768 }
769 }
770 }
771 }
772 Ok(args[0].clone())
773 })),
774 );
775
776 env.set(
778 intern("__vm-prompt"),
779 Value::native_fn(NativeFn::simple("__vm-prompt", |args| {
780 use sema_core::{Message, Prompt, Role};
781 if args.len() != 1 {
782 return Err(SemaError::arity("__vm-prompt", "1", args.len()));
783 }
784 let entries = args[0]
785 .as_list()
786 .ok_or_else(|| SemaError::type_error("list", args[0].type_name()))?;
787 let mut messages = Vec::new();
788 for entry in entries {
789 if let Some(msg) = entry.as_message_rc() {
790 messages.push((*msg).clone());
791 } else if let Some(pair) = entry.as_list() {
792 if pair.len() == 2 {
793 let role_str = pair[0]
794 .as_str()
795 .ok_or_else(|| SemaError::eval("prompt: expected role string"))?;
796 let role = match role_str {
797 "system" => Role::System,
798 "user" => Role::User,
799 "assistant" => Role::Assistant,
800 "tool" => Role::Tool,
801 other => {
802 return Err(SemaError::eval(format!(
803 "prompt: unknown role '{other}'"
804 )))
805 }
806 };
807 let parts = pair[1]
808 .as_list()
809 .ok_or_else(|| SemaError::type_error("list", pair[1].type_name()))?;
810 let mut content = String::new();
811 for part in parts {
812 if let Some(s) = part.as_str() {
813 content.push_str(s);
814 } else {
815 content.push_str(&part.to_string());
816 }
817 }
818 messages.push(Message {
819 role,
820 content,
821 images: Vec::new(),
822 });
823 } else {
824 return Err(SemaError::eval(
825 "prompt: expected (role parts) pair or message value",
826 ));
827 }
828 } else {
829 return Err(SemaError::eval(
830 "prompt: expected (role parts) pair or message value",
831 ));
832 }
833 }
834 Ok(Value::prompt(Prompt { messages }))
835 })),
836 );
837
838 env.set(
840 intern("__vm-message"),
841 Value::native_fn(NativeFn::simple("__vm-message", |args| {
842 use sema_core::{Message, Role};
843 if args.len() != 2 {
844 return Err(SemaError::arity("__vm-message", "2", args.len()));
845 }
846 let role = if let Some(spur) = args[0].as_keyword_spur() {
847 let s = resolve(spur);
848 match s.as_str() {
849 "system" => Role::System,
850 "user" => Role::User,
851 "assistant" => Role::Assistant,
852 "tool" => Role::Tool,
853 other => {
854 return Err(SemaError::eval(format!("message: unknown role '{other}'")))
855 }
856 }
857 } else {
858 return Err(SemaError::type_error("keyword", args[0].type_name()));
859 };
860 let parts = args[1]
861 .as_list()
862 .ok_or_else(|| SemaError::type_error("list", args[1].type_name()))?;
863 let mut content = String::new();
864 for part in parts {
865 if let Some(s) = part.as_str() {
866 content.push_str(s);
867 } else {
868 content.push_str(&part.to_string());
869 }
870 }
871 Ok(Value::message(Message {
872 role,
873 content,
874 images: Vec::new(),
875 }))
876 })),
877 );
878
879 let tool_env = env.clone();
884 env.set(
885 intern("__vm-deftool"),
886 Value::native_fn(NativeFn::simple("__vm-deftool", move |args| {
887 if args.len() != 4 {
888 return Err(SemaError::arity("deftool", "4", args.len()));
889 }
890 let name = args[0]
891 .as_symbol()
892 .ok_or_else(|| SemaError::eval("deftool: name must be a symbol"))?;
893 special_forms::register_tool(
894 &name,
895 args[1].clone(),
896 args[2].clone(),
897 args[3].clone(),
898 &tool_env,
899 )
900 })),
901 );
902
903 let agent_env = env.clone();
906 env.set(
907 intern("__vm-defagent"),
908 Value::native_fn(NativeFn::simple("__vm-defagent", move |args| {
909 if args.len() != 2 {
910 return Err(SemaError::arity("defagent", "2", args.len()));
911 }
912 let name = args[0]
913 .as_symbol()
914 .ok_or_else(|| SemaError::eval("defagent: name must be a symbol"))?;
915 special_forms::register_agent(&name, args[1].clone(), &agent_env)
916 })),
917 );
918
919 env.set(
922 intern("__vm-destructure"),
923 Value::native_fn(NativeFn::simple("__vm-destructure", |args| {
924 if args.len() != 2 {
925 return Err(SemaError::arity("__vm-destructure", "2", args.len()));
926 }
927 let bindings = crate::destructure::destructure(&args[0], &args[1])?;
928 let mut map = std::collections::BTreeMap::new();
929 for (spur, val) in bindings {
930 map.insert(Value::symbol_from_spur(spur), val);
931 }
932 Ok(Value::map(map))
933 })),
934 );
935
936 env.set(
939 intern("__vm-try-match"),
940 Value::native_fn(NativeFn::simple("__vm-try-match", |args| {
941 if args.len() != 2 {
942 return Err(SemaError::arity("__vm-try-match", "2", args.len()));
943 }
944 match crate::destructure::try_match(&args[0], &args[1])? {
945 Some(bindings) => {
946 let mut map = std::collections::BTreeMap::new();
947 for (spur, val) in bindings {
948 map.insert(Value::symbol_from_spur(spur), val);
949 }
950 Ok(Value::map(map))
951 }
952 None => Ok(Value::nil()),
953 }
954 })),
955 );
956
957 env.set(
961 intern("__vm-match-failed"),
962 Value::native_fn(NativeFn::simple("__vm-match-failed", |args| {
963 let val = args.first().cloned().unwrap_or_else(Value::nil);
964 Err(
965 SemaError::eval(format!("match: no clause matched value: {val}")).with_hint(
966 "add a catch-all `(_ ...)` clause, or use `match*` to return nil on no match",
967 ),
968 )
969 })),
970 );
971
972 env.set(
974 intern("__vm-make-multi"),
975 Value::native_fn(NativeFn::simple("__vm-make-multi", |args| {
976 if args.len() != 2 {
977 return Err(SemaError::arity("__vm-make-multi", "2", args.len()));
978 }
979 let name_spur = args[0]
980 .as_symbol_spur()
981 .ok_or_else(|| SemaError::eval("__vm-make-multi: expected symbol"))?;
982 Ok(Value::multimethod(MultiMethod {
983 name: name_spur,
984 dispatch_fn: args[1].clone(),
985 methods: RefCell::new(std::collections::BTreeMap::new()),
986 default: RefCell::new(None),
987 }))
988 })),
989 );
990
991 env.set(
993 intern("__vm-defmethod"),
994 Value::native_fn(NativeFn::simple("__vm-defmethod", |args| {
995 if args.len() != 3 {
996 return Err(SemaError::arity("__vm-defmethod", "3", args.len()));
997 }
998 let mm = args[0]
999 .as_multimethod_rc()
1000 .ok_or_else(|| SemaError::eval("defmethod: first argument is not a multimethod"))?;
1001 let dispatch_val = &args[1];
1002 let handler = &args[2];
1003 if let Some(kw) = dispatch_val.as_keyword_spur() {
1004 if resolve(kw) == "default" {
1005 *mm.default.borrow_mut() = Some(handler.clone());
1006 return Ok(Value::nil());
1007 }
1008 }
1009 mm.methods
1010 .borrow_mut()
1011 .insert(dispatch_val.clone(), handler.clone());
1012 Ok(Value::nil())
1013 })),
1014 );
1015}