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