Skip to main content

sema_eval/
eval.rs

1use std::cell::RefCell;
2use std::collections::HashSet;
3use std::rc::Rc;
4
5use sema_core::{
6    intern, resolve, CallFrame, Env, EvalContext, Lambda, Macro, MultiMethod, NativeFn, SemaError,
7    Span, Spur, Thunk, Value, ValueView,
8};
9
10use crate::special_forms;
11
12/// Trampoline for tail-call optimization.
13pub enum Trampoline {
14    Value(Value),
15    Eval(Value, Env),
16}
17
18pub type EvalResult = Result<Value, SemaError>;
19
20/// Create an isolated module env: child of root (global/stdlib) env
21pub fn create_module_env(env: &Env) -> Env {
22    // Walk parent chain to find root
23    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
34/// Look up a span for an expression via the span table in the context.
35fn span_of_expr(ctx: &EvalContext, expr: &Value) -> Option<Span> {
36    if let Some(items) = expr.as_list_rc() {
37        let ptr = Rc::as_ptr(&items) as usize;
38        ctx.lookup_span(ptr)
39    } else {
40        None
41    }
42}
43
44/// RAII guard that truncates the call stack on drop.
45struct CallStackGuard<'a> {
46    ctx: &'a EvalContext,
47    entry_depth: usize,
48}
49
50impl Drop for CallStackGuard<'_> {
51    fn drop(&mut self) {
52        self.ctx.truncate_call_stack(self.entry_depth);
53    }
54}
55
56/// Collect the names of all native functions in an environment.
57/// Used to tell the bytecode compiler which globals can use CallNative.
58fn collect_native_names(env: &Env) -> HashSet<Spur> {
59    env.all_names()
60        .into_iter()
61        .filter(|&spur| env.get(spur).is_some_and(|v| v.is_native_fn()))
62        .collect()
63}
64
65/// The interpreter holds the global environment and state.
66pub struct Interpreter {
67    pub global_env: Rc<Env>,
68    pub ctx: EvalContext,
69}
70
71impl Default for Interpreter {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl Interpreter {
78    pub fn new() -> Self {
79        let env = Env::new();
80        let ctx = EvalContext::new();
81        // Register eval/call callbacks so stdlib can invoke the real evaluator
82        sema_core::set_eval_callback(&ctx, eval_value);
83        sema_core::set_call_callback(&ctx, call_value);
84        // Register stdlib
85        sema_stdlib::register_stdlib(&env, &sema_core::Sandbox::allow_all());
86        // Register LLM builtins
87        #[cfg(not(target_arch = "wasm32"))]
88        {
89            sema_llm::builtins::reset_runtime_state();
90            sema_llm::builtins::register_llm_builtins(&env, &sema_core::Sandbox::allow_all());
91            sema_llm::builtins::set_eval_callback(eval_value);
92        }
93        let global_env = Rc::new(env);
94        register_vm_delegates(&global_env);
95        load_prelude(&ctx, &global_env);
96        Interpreter { global_env, ctx }
97    }
98
99    pub fn new_with_sandbox(sandbox: &sema_core::Sandbox) -> Self {
100        let env = Env::new();
101        let ctx = EvalContext::new_with_sandbox(sandbox.clone());
102        sema_core::set_eval_callback(&ctx, eval_value);
103        sema_core::set_call_callback(&ctx, call_value);
104        sema_stdlib::register_stdlib(&env, sandbox);
105        #[cfg(not(target_arch = "wasm32"))]
106        {
107            sema_llm::builtins::reset_runtime_state();
108            sema_llm::builtins::register_llm_builtins(&env, sandbox);
109            sema_llm::builtins::set_eval_callback(eval_value);
110        }
111        let global_env = Rc::new(env);
112        register_vm_delegates(&global_env);
113        load_prelude(&ctx, &global_env);
114        Interpreter { global_env, ctx }
115    }
116
117    pub fn eval(&self, expr: &Value) -> EvalResult {
118        eval_value(&self.ctx, expr, &Env::with_parent(self.global_env.clone()))
119    }
120
121    pub fn eval_str(&self, input: &str) -> EvalResult {
122        eval_string(&self.ctx, input, &Env::with_parent(self.global_env.clone()))
123    }
124
125    /// Evaluate in the global environment so that `define` persists across calls.
126    pub fn eval_in_global(&self, expr: &Value) -> EvalResult {
127        eval_value(&self.ctx, expr, &self.global_env)
128    }
129
130    /// Parse and evaluate in the global environment so that `define` persists across calls.
131    pub fn eval_str_in_global(&self, input: &str) -> EvalResult {
132        eval_string(&self.ctx, input, &self.global_env)
133    }
134
135    /// Parse, compile to bytecode, and execute via the VM.
136    pub fn eval_str_compiled(&self, input: &str) -> EvalResult {
137        let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
138        self.ctx.merge_span_table(spans);
139        if exprs.is_empty() {
140            return Ok(Value::nil());
141        }
142
143        let mut expanded = Vec::new();
144        for expr in &exprs {
145            let exp = self.expand_for_vm(expr)?;
146            expanded.push(exp);
147        }
148
149        let known_natives = collect_native_names(&self.global_env);
150        let prog = sema_vm::compile_program(&expanded, Some(known_natives))?;
151        let mut vm = sema_vm::VM::new(
152            self.global_env.clone(),
153            prog.functions,
154            &prog.native_table,
155            prog.main_cache_slots,
156        )?;
157        vm.execute(prog.closure, &self.ctx)
158    }
159
160    /// Compile source code to bytecode without executing.
161    /// Handles macro expansion (defmacro + macro calls) before compilation.
162    pub fn compile_to_bytecode(&self, input: &str) -> Result<sema_vm::CompileResult, SemaError> {
163        let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
164        self.ctx.merge_span_table(spans);
165
166        let mut expanded = Vec::new();
167        for expr in &exprs {
168            let exp = self.expand_for_vm(expr)?;
169            if !exp.is_nil() {
170                expanded.push(exp);
171            }
172        }
173
174        if expanded.is_empty() {
175            expanded.push(Value::nil());
176        }
177
178        let prog = sema_vm::compile_program(&expanded, None)?;
179        Ok(sema_vm::CompileResult::new(
180            prog.closure.func.chunk.clone(),
181            prog.functions.iter().map(|f| (**f).clone()).collect(),
182        ))
183    }
184
185    /// Pre-process a top-level expression for VM compilation.
186    /// Evaluates `defmacro` forms via the tree-walker to register macros,
187    /// then expands macro calls in all other forms.
188    pub fn expand_for_vm(&self, expr: &Value) -> EvalResult {
189        if let Some(items) = expr.as_list() {
190            if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
191                let name = resolve(s);
192                if name == "defmacro" {
193                    eval_value(&self.ctx, expr, &self.global_env)?;
194                    return Ok(Value::nil());
195                }
196                if name == "begin" || name == "progn" {
197                    let mut new_items = vec![Value::symbol_from_spur(s)];
198                    let mut changed = false;
199                    for item in &items[1..] {
200                        let expanded = self.expand_for_vm(item)?;
201                        if expanded.raw_bits() != item.raw_bits() {
202                            changed = true;
203                        }
204                        new_items.push(expanded);
205                    }
206                    if !changed {
207                        return Ok(expr.clone());
208                    }
209                    return Ok(Value::list(new_items));
210                }
211            }
212        }
213        self.expand_macros(expr)
214    }
215
216    /// Recursively expand macro calls in an expression.
217    /// Preserves Rc pointer identity when no actual expansion occurs,
218    /// so that span lookups (keyed by Rc pointer) remain valid.
219    fn expand_macros(&self, expr: &Value) -> EvalResult {
220        if let Some(items) = expr.as_list() {
221            if !items.is_empty() {
222                if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
223                    let name = resolve(s);
224                    if name == "quote" {
225                        return Ok(expr.clone());
226                    }
227                    if let Some(mac_val) = self.global_env.get(s) {
228                        if let Some(mac) = mac_val.as_macro_rc() {
229                            let expanded =
230                                apply_macro(&self.ctx, &mac, &items[1..], &self.global_env)?;
231                            return self.expand_macros(&expanded);
232                        }
233                    }
234                }
235                let expanded: Vec<Value> = items
236                    .iter()
237                    .map(|v| self.expand_macros(v))
238                    .collect::<Result<_, _>>()?;
239                // If no item changed, return original to preserve Rc identity (and spans)
240                let changed = expanded
241                    .iter()
242                    .zip(items.iter())
243                    .any(|(a, b)| a.raw_bits() != b.raw_bits());
244                if !changed {
245                    return Ok(expr.clone());
246                }
247                return Ok(Value::list(expanded));
248            }
249        }
250        Ok(expr.clone())
251    }
252}
253
254/// Evaluate a string containing one or more expressions.
255pub fn eval_string(ctx: &EvalContext, input: &str, env: &Env) -> EvalResult {
256    let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
257    ctx.merge_span_table(spans);
258    ctx.max_eval_depth.set(0);
259    let mut result = Value::nil();
260    for expr in &exprs {
261        result = eval_value(ctx, expr, env)?;
262    }
263    Ok(result)
264}
265
266/// The core eval function: evaluate a Value in an environment.
267pub fn eval(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
268    eval_value(ctx, expr, env)
269}
270
271/// Maximum eval nesting depth before we bail with an error.
272/// This prevents native stack overflow from unbounded recursion
273/// (both function calls and special form nesting like deeply nested if/let/begin).
274/// WASM has a much smaller call stack (~1MB V8 limit) so we use a lower depth.
275#[cfg(target_arch = "wasm32")]
276const MAX_EVAL_DEPTH: usize = 256;
277#[cfg(not(target_arch = "wasm32"))]
278const MAX_EVAL_DEPTH: usize = 1024;
279
280pub fn eval_value(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
281    // Fast path: self-evaluating forms skip depth/step tracking entirely.
282    match expr.view() {
283        ValueView::Nil
284        | ValueView::Bool(_)
285        | ValueView::Int(_)
286        | ValueView::Float(_)
287        | ValueView::String(_)
288        | ValueView::Char(_)
289        | ValueView::Keyword(_)
290        | ValueView::Thunk(_)
291        | ValueView::Bytevector(_)
292        | ValueView::NativeFn(_)
293        | ValueView::Lambda(_)
294        | ValueView::HashMap(_) => return Ok(expr.clone()),
295        ValueView::Symbol(spur) => {
296            if let Some(val) = env.get(spur) {
297                return Ok(val);
298            }
299            let name = resolve(spur);
300            let mut err = SemaError::Unbound(name.clone());
301            // Check for common names from other Lisp dialects first
302            if let Some(hint) = sema_core::error::veteran_hint(&name) {
303                err = err.with_hint(hint);
304            } else {
305                // Fall back to fuzzy matching
306                let all_names: Vec<String> = env.all_names().iter().map(|s| resolve(*s)).collect();
307                let candidates: Vec<&str> = all_names.iter().map(|s| s.as_str()).collect();
308                if let Some(suggestion) = sema_core::error::suggest_similar(&name, &candidates) {
309                    err = err.with_hint(format!("Did you mean '{suggestion}'?"));
310                }
311            }
312            let trace = ctx.capture_stack_trace();
313            return Err(err.with_stack_trace(trace));
314        }
315        _ => {}
316    }
317
318    let depth = ctx.eval_depth.get();
319    ctx.eval_depth.set(depth + 1);
320    if depth + 1 > ctx.max_eval_depth.get() {
321        ctx.max_eval_depth.set(depth + 1);
322    }
323    if depth == 0 {
324        ctx.eval_steps.set(0);
325    }
326    if depth > MAX_EVAL_DEPTH {
327        ctx.eval_depth.set(ctx.eval_depth.get().saturating_sub(1));
328        return Err(SemaError::eval(format!(
329            "maximum eval depth exceeded ({MAX_EVAL_DEPTH})"
330        )).with_hint("this usually means infinite recursion; ensure recursive calls are in tail position for TCO, or use 'do' for iteration"));
331    }
332
333    let result = eval_value_inner(ctx, expr, env);
334
335    ctx.eval_depth.set(ctx.eval_depth.get().saturating_sub(1));
336    result
337}
338
339/// Call a function value with already-evaluated arguments.
340/// This is the public API for stdlib functions that need to invoke callbacks.
341///
342/// For lambdas, this delegates to `apply_lambda` + a trampoline loop so that
343/// subsequent evaluation happens iteratively rather than adding Rust stack
344/// frames.  This is critical for WASM where the call stack is limited (~5 MB).
345pub fn call_value(ctx: &EvalContext, func: &Value, args: &[Value]) -> EvalResult {
346    match func.view() {
347        ValueView::NativeFn(native) => (native.func)(ctx, args),
348        ValueView::Lambda(lambda) => {
349            let trampoline = apply_lambda(ctx, &lambda, args)?;
350            run_trampoline(ctx, trampoline)
351        }
352        ValueView::Keyword(spur) => {
353            if args.len() != 1 {
354                let name = resolve(spur);
355                return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
356            }
357            let key = Value::keyword_from_spur(spur);
358            match args[0].view() {
359                ValueView::Map(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
360                ValueView::HashMap(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
361                _ => Err(SemaError::type_error_with_value(
362                    "map",
363                    args[0].type_name(),
364                    &args[0],
365                )),
366            }
367        }
368        ValueView::MultiMethod(mm) => call_multimethod(ctx, &mm, args),
369        _ => Err(
370            SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
371                .with_hint("expected a function, lambda, or keyword"),
372        ),
373    }
374}
375
376/// Call a multimethod: dispatch on args, look up handler, call it.
377fn call_multimethod(ctx: &EvalContext, mm: &Rc<MultiMethod>, args: &[Value]) -> EvalResult {
378    let dispatch_val = call_value(ctx, &mm.dispatch_fn, args)?;
379    let methods = mm.methods.borrow();
380    if let Some(handler) = methods.get(&dispatch_val) {
381        let handler = handler.clone();
382        drop(methods);
383        call_value(ctx, &handler, args)
384    } else {
385        drop(methods);
386        let default = mm.default.borrow().clone();
387        if let Some(handler) = default {
388            call_value(ctx, &handler, args)
389        } else {
390            Err(SemaError::eval(format!(
391                "no method in multimethod '{}' for dispatch value: {}",
392                resolve(mm.name),
393                dispatch_val
394            ))
395            .with_hint("add a (defmethod name :default handler) to handle unmatched values"))
396        }
397    }
398}
399
400/// Run a trampoline to completion iteratively.
401/// Used by `call_value` so that stdlib HOF callbacks (map, for-each, etc.)
402/// don't grow the Rust call stack for every evaluation step.
403fn run_trampoline(ctx: &EvalContext, trampoline: Trampoline) -> EvalResult {
404    let limit = ctx.eval_step_limit.get();
405    let mut current = trampoline;
406    loop {
407        match current {
408            Trampoline::Value(v) => return Ok(v),
409            Trampoline::Eval(expr, env) => {
410                if limit > 0 {
411                    let v = ctx.eval_steps.get() + 1;
412                    ctx.eval_steps.set(v);
413                    if v > limit {
414                        return Err(SemaError::eval("eval step limit exceeded".to_string()));
415                    }
416                }
417                match eval_step(ctx, &expr, &env) {
418                    Ok(t) => current = t,
419                    Err(e) => {
420                        if e.stack_trace().is_none() {
421                            let trace = ctx.capture_stack_trace();
422                            return Err(e.with_stack_trace(trace));
423                        }
424                        return Err(e);
425                    }
426                }
427            }
428        }
429    }
430}
431
432fn eval_value_inner(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
433    let entry_depth = ctx.call_stack_depth();
434    let guard = CallStackGuard { ctx, entry_depth };
435    let limit = ctx.eval_step_limit.get();
436
437    // First iteration: use borrowed expr/env to avoid cloning
438    if limit > 0 {
439        let v = ctx.eval_steps.get() + 1;
440        ctx.eval_steps.set(v);
441        if v > limit {
442            return Err(SemaError::eval("eval step limit exceeded".to_string()));
443        }
444    }
445
446    match eval_step(ctx, expr, env) {
447        Ok(Trampoline::Value(v)) => {
448            drop(guard);
449            Ok(v)
450        }
451        Ok(Trampoline::Eval(next_expr, next_env)) => {
452            // Need to continue — enter the trampoline loop
453            let mut current_expr = next_expr;
454            let mut current_env = next_env;
455
456            // Trim call stack for TCO
457            {
458                let mut stack = ctx.call_stack.borrow_mut();
459                if stack.len() > entry_depth + 1 {
460                    let top = stack.last().cloned();
461                    stack.truncate(entry_depth);
462                    if let Some(frame) = top {
463                        stack.push(frame);
464                    }
465                }
466            }
467
468            loop {
469                if limit > 0 {
470                    let v = ctx.eval_steps.get() + 1;
471                    ctx.eval_steps.set(v);
472                    if v > limit {
473                        return Err(SemaError::eval("eval step limit exceeded".to_string()));
474                    }
475                }
476
477                match eval_step(ctx, &current_expr, &current_env) {
478                    Ok(Trampoline::Value(v)) => {
479                        drop(guard);
480                        return Ok(v);
481                    }
482                    Ok(Trampoline::Eval(next_expr, next_env)) => {
483                        {
484                            let mut stack = ctx.call_stack.borrow_mut();
485                            if stack.len() > entry_depth + 1 {
486                                let top = stack.last().cloned();
487                                stack.truncate(entry_depth);
488                                if let Some(frame) = top {
489                                    stack.push(frame);
490                                }
491                            }
492                        }
493                        current_expr = next_expr;
494                        current_env = next_env;
495                    }
496                    Err(e) => {
497                        if e.stack_trace().is_none() {
498                            let trace = ctx.capture_stack_trace();
499                            drop(guard);
500                            return Err(e.with_stack_trace(trace));
501                        }
502                        drop(guard);
503                        return Err(e);
504                    }
505                }
506            }
507        }
508        Err(e) => {
509            if e.stack_trace().is_none() {
510                let trace = ctx.capture_stack_trace();
511                drop(guard);
512                return Err(e.with_stack_trace(trace));
513            }
514            drop(guard);
515            Err(e)
516        }
517    }
518}
519
520fn eval_step(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Trampoline, SemaError> {
521    match expr.view() {
522        // Self-evaluating forms
523        ValueView::Nil
524        | ValueView::Bool(_)
525        | ValueView::Int(_)
526        | ValueView::Float(_)
527        | ValueView::String(_)
528        | ValueView::Char(_)
529        | ValueView::Thunk(_)
530        | ValueView::Bytevector(_) => Ok(Trampoline::Value(expr.clone())),
531        ValueView::Keyword(_) => Ok(Trampoline::Value(expr.clone())),
532        ValueView::Vector(items) => {
533            let mut result = Vec::with_capacity(items.len());
534            for item in items.iter() {
535                result.push(eval_value(ctx, item, env)?);
536            }
537            Ok(Trampoline::Value(Value::vector(result)))
538        }
539        ValueView::Map(map) => {
540            let mut result = std::collections::BTreeMap::new();
541            for (k, v) in map.iter() {
542                let ek = eval_value(ctx, k, env)?;
543                let ev = eval_value(ctx, v, env)?;
544                result.insert(ek, ev);
545            }
546            Ok(Trampoline::Value(Value::map(result)))
547        }
548        ValueView::HashMap(_) => Ok(Trampoline::Value(expr.clone())),
549
550        // Symbol lookup
551        ValueView::Symbol(spur) => env.get(spur).map(Trampoline::Value).ok_or_else(|| {
552            let name = resolve(spur);
553            let mut err = SemaError::Unbound(name.clone());
554            if let Some(hint) = sema_core::error::veteran_hint(&name) {
555                err = err.with_hint(hint);
556            } else {
557                let all_names: Vec<String> = env.all_names().iter().map(|s| resolve(*s)).collect();
558                let candidates: Vec<&str> = all_names.iter().map(|s| s.as_str()).collect();
559                if let Some(suggestion) = sema_core::error::suggest_similar(&name, &candidates) {
560                    err = err.with_hint(format!("Did you mean '{suggestion}'?"));
561                }
562            }
563            err
564        }),
565
566        // Function application / special forms
567        ValueView::List(items) => {
568            if items.is_empty() {
569                return Ok(Trampoline::Value(Value::nil()));
570            }
571
572            let head = &items[0];
573            let args = &items[1..];
574
575            // O(1) special form dispatch: compare the symbol's Spur (u32 interned handle)
576            // against cached constants, avoiding string resolution entirely.
577            if let Some(spur) = head.as_symbol_spur() {
578                if let Some(result) = special_forms::try_eval_special(spur, args, env, ctx) {
579                    return result;
580                }
581            }
582
583            // Evaluate the head to get the callable
584            let func = eval_value(ctx, head, env)?;
585
586            // Look up the span of the call site expression
587            let call_span = span_of_expr(ctx, expr);
588
589            match func.view() {
590                ValueView::NativeFn(native) => {
591                    // Evaluate arguments
592                    let mut eval_args = Vec::with_capacity(args.len());
593                    for arg in args {
594                        eval_args.push(eval_value(ctx, arg, env)?);
595                    }
596                    // Push frame, call native fn
597                    let frame = CallFrame {
598                        name: native.name.to_string(),
599                        file: ctx.current_file_path(),
600                        span: call_span,
601                    };
602                    ctx.push_call_frame(frame);
603                    match (native.func)(ctx, &eval_args) {
604                        Ok(v) => {
605                            // Pop on success (native fns don't trampoline)
606                            ctx.truncate_call_stack(ctx.call_stack_depth().saturating_sub(1));
607                            Ok(Trampoline::Value(v))
608                        }
609                        // On error, leave frame for stack trace capture
610                        Err(e) => Err(annotate_arity_error(e, expr)),
611                    }
612                }
613                ValueView::Lambda(lambda) => {
614                    // Evaluate arguments
615                    let mut eval_args = Vec::with_capacity(args.len());
616                    for arg in args {
617                        eval_args.push(eval_value(ctx, arg, env)?);
618                    }
619                    // Push frame — trampoline continues, eval_value guard handles cleanup
620                    let frame = CallFrame {
621                        name: lambda
622                            .name
623                            .map(resolve)
624                            .unwrap_or_else(|| "<lambda>".to_string()),
625                        file: ctx.current_file_path(),
626                        span: call_span,
627                    };
628                    ctx.push_call_frame(frame);
629                    apply_lambda(ctx, &lambda, &eval_args)
630                        .map_err(|e| annotate_arity_error(e, expr))
631                }
632                ValueView::Macro(mac) => {
633                    // Macros receive unevaluated arguments
634                    let expanded = apply_macro(ctx, &mac, args, env)?;
635                    // Evaluate the expansion in the current env (TCO)
636                    Ok(Trampoline::Eval(expanded, env.clone()))
637                }
638                ValueView::Keyword(spur) => {
639                    // Keywords as functions: (:key map) => (get map :key)
640                    if args.len() != 1 {
641                        let name = resolve(spur);
642                        return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
643                    }
644                    let map_val = eval_value(ctx, &args[0], env)?;
645                    let key = Value::keyword_from_spur(spur);
646                    match map_val.view() {
647                        ValueView::Map(map) => Ok(Trampoline::Value(
648                            map.get(&key).cloned().unwrap_or(Value::nil()),
649                        )),
650                        ValueView::HashMap(map) => Ok(Trampoline::Value(
651                            map.get(&key).cloned().unwrap_or(Value::nil()),
652                        )),
653                        _ => Err(SemaError::type_error_with_value(
654                            "map",
655                            map_val.type_name(),
656                            &map_val,
657                        )),
658                    }
659                }
660                ValueView::MultiMethod(mm) => {
661                    let mut eval_args = Vec::with_capacity(args.len());
662                    for arg in args {
663                        eval_args.push(eval_value(ctx, arg, env)?);
664                    }
665                    let result = call_multimethod(ctx, &mm, &eval_args)?;
666                    Ok(Trampoline::Value(result))
667                }
668                _ => Err(
669                    SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
670                        .with_hint("the first element of a list must be a function or macro"),
671                ),
672            }
673        }
674
675        _other => Ok(Trampoline::Value(expr.clone())),
676    }
677}
678
679/// If `err` is an arity error, attach a note showing the original call form.
680fn annotate_arity_error(err: SemaError, expr: &Value) -> SemaError {
681    if matches!(err.inner(), SemaError::Arity { .. }) && err.note().is_none() {
682        let form_str = format!("{}", expr);
683        let truncated = if form_str.len() > 80 {
684            format!("{}…", &form_str[..79])
685        } else {
686            form_str
687        };
688        err.with_note(format!("in: {truncated}"))
689    } else {
690        err
691    }
692}
693
694/// Apply a lambda to evaluated arguments with TCO.
695fn apply_lambda(
696    ctx: &EvalContext,
697    lambda: &Rc<Lambda>,
698    args: &[Value],
699) -> Result<Trampoline, SemaError> {
700    let new_env = Env::with_parent(Rc::new(lambda.env.clone()));
701
702    // Bind parameters
703    if let Some(rest) = lambda.rest_param {
704        if args.len() < lambda.params.len() {
705            return Err(SemaError::arity(
706                lambda
707                    .name
708                    .map(resolve)
709                    .unwrap_or_else(|| "lambda".to_string()),
710                format!("{}+", lambda.params.len()),
711                args.len(),
712            ));
713        }
714        for (param, arg) in lambda.params.iter().zip(args.iter()) {
715            new_env.set(*param, arg.clone());
716        }
717        let rest_args = args[lambda.params.len()..].to_vec();
718        new_env.set(rest, Value::list(rest_args));
719    } else {
720        if args.len() != lambda.params.len() {
721            return Err(SemaError::arity(
722                lambda
723                    .name
724                    .map(resolve)
725                    .unwrap_or_else(|| "lambda".to_string()),
726                lambda.params.len().to_string(),
727                args.len(),
728            ));
729        }
730        for (param, arg) in lambda.params.iter().zip(args.iter()) {
731            new_env.set(*param, arg.clone());
732        }
733    }
734
735    // Self-reference for recursion — just clone the Rc pointer
736    if let Some(name) = lambda.name {
737        new_env.set(name, Value::lambda_from_rc(Rc::clone(lambda)));
738    }
739
740    // Evaluate body with TCO on last expression
741    if lambda.body.is_empty() {
742        return Ok(Trampoline::Value(Value::nil()));
743    }
744    for expr in &lambda.body[..lambda.body.len() - 1] {
745        eval_value(ctx, expr, &new_env)?;
746    }
747    Ok(Trampoline::Eval(
748        lambda.body.last().unwrap().clone(),
749        new_env,
750    ))
751}
752
753/// Apply a macro: bind unevaluated args, evaluate body to produce expansion.
754pub fn apply_macro(
755    ctx: &EvalContext,
756    mac: &sema_core::Macro,
757    args: &[Value],
758    caller_env: &Env,
759) -> Result<Value, SemaError> {
760    let env = Env::with_parent(Rc::new(caller_env.clone()));
761
762    // Bind parameters to unevaluated forms
763    if let Some(rest) = mac.rest_param {
764        if args.len() < mac.params.len() {
765            return Err(SemaError::arity(
766                resolve(mac.name),
767                format!("{}+", mac.params.len()),
768                args.len(),
769            ));
770        }
771        for (param, arg) in mac.params.iter().zip(args.iter()) {
772            env.set(*param, arg.clone());
773        }
774        let rest_args = args[mac.params.len()..].to_vec();
775        env.set(rest, Value::list(rest_args));
776    } else {
777        if args.len() != mac.params.len() {
778            return Err(SemaError::arity(
779                resolve(mac.name),
780                mac.params.len().to_string(),
781                args.len(),
782            ));
783        }
784        for (param, arg) in mac.params.iter().zip(args.iter()) {
785            env.set(*param, arg.clone());
786        }
787    }
788
789    // Evaluate the macro body to get the expansion
790    let mut result = Value::nil();
791    for expr in &mac.body {
792        result = eval_value(ctx, expr, &env)?;
793    }
794    Ok(result)
795}
796
797/// Register `__vm-*` native functions that the bytecode VM calls back into
798/// the tree-walker for forms that cannot be fully compiled.
799/// Load built-in macros (threading, when-let, if-let) into the global environment.
800fn load_prelude(ctx: &EvalContext, env: &Rc<Env>) {
801    let exprs = sema_reader::read_many(crate::prelude::PRELUDE).expect("prelude parse error");
802    for expr in &exprs {
803        eval_value(ctx, expr, env).expect("prelude eval error");
804    }
805}
806
807fn register_vm_delegates(env: &Rc<Env>) {
808    // __vm-eval: evaluate an expression via the tree-walker
809    let eval_env = env.clone();
810    env.set(
811        intern("__vm-eval"),
812        Value::native_fn(NativeFn::with_ctx("__vm-eval", move |ctx, args| {
813            if args.len() != 1 {
814                return Err(SemaError::arity("eval", "1", args.len()));
815            }
816            sema_core::eval_callback(ctx, &args[0], &eval_env)
817        })),
818    );
819
820    // __vm-load: delegate to the tree-walker's eval_load via eval_callback,
821    // mirroring how __vm-import delegates to eval_import. This ensures VFS
822    // resolution, file path push/pop, and all other load semantics are
823    // handled by a single code path in special_forms.rs.
824    let load_env = env.clone();
825    env.set(
826        intern("__vm-load"),
827        Value::native_fn(NativeFn::with_ctx("__vm-load", move |ctx, args| {
828            if args.len() != 1 {
829                return Err(SemaError::arity("load", "1", args.len()));
830            }
831            let load_expr = Value::list(vec![Value::symbol("load"), args[0].clone()]);
832            sema_core::eval_callback(ctx, &load_expr, &load_env)
833        })),
834    );
835
836    // __vm-import: import a module via the tree-walker
837    let import_env = env.clone();
838    env.set(
839        intern("__vm-import"),
840        Value::native_fn(NativeFn::with_ctx("__vm-import", move |ctx, args| {
841            if args.len() != 2 {
842                return Err(SemaError::arity("import", "2", args.len()));
843            }
844            ctx.sandbox.check(sema_core::Caps::FS_READ, "import")?;
845            let mut form = vec![Value::symbol("import"), args[0].clone()];
846            if let Some(items) = args[1].as_list() {
847                if !items.is_empty() {
848                    for item in items.iter() {
849                        form.push(item.clone());
850                    }
851                }
852            }
853            let import_expr = Value::list(form);
854            sema_core::eval_callback(ctx, &import_expr, &import_env)
855        })),
856    );
857
858    // __vm-defmacro: register a macro in the environment
859    let macro_env = env.clone();
860    env.set(
861        intern("__vm-defmacro"),
862        Value::native_fn(NativeFn::simple("__vm-defmacro", move |args| {
863            if args.len() != 4 {
864                return Err(SemaError::arity("defmacro", "4", args.len()));
865            }
866            let name = match args[0].as_symbol_spur() {
867                Some(s) => s,
868                None => return Err(SemaError::type_error("symbol", args[0].type_name())),
869            };
870            let params = match args[1].as_list() {
871                Some(items) => items
872                    .iter()
873                    .map(|v| match v.as_symbol_spur() {
874                        Some(s) => Ok(s),
875                        None => Err(SemaError::type_error("symbol", v.type_name())),
876                    })
877                    .collect::<Result<Vec<_>, _>>()?,
878                None => return Err(SemaError::type_error("list", args[1].type_name())),
879            };
880            let rest_param = if let Some(s) = args[2].as_symbol_spur() {
881                Some(s)
882            } else if args[2].is_nil() {
883                None
884            } else {
885                return Err(SemaError::type_error("symbol or nil", args[2].type_name()));
886            };
887            let body = vec![args[3].clone()];
888            macro_env.set(
889                name,
890                Value::macro_val(Macro {
891                    params,
892                    rest_param,
893                    body,
894                    name,
895                }),
896            );
897            Ok(Value::nil())
898        })),
899    );
900
901    // __vm-defmacro-form: delegate complete defmacro form to the tree-walker
902    let dmf_env = env.clone();
903    env.set(
904        intern("__vm-defmacro-form"),
905        Value::native_fn(NativeFn::with_ctx(
906            "__vm-defmacro-form",
907            move |ctx, args| {
908                if args.len() != 1 {
909                    return Err(SemaError::arity("defmacro-form", "1", args.len()));
910                }
911                sema_core::eval_callback(ctx, &args[0], &dmf_env)
912            },
913        )),
914    );
915
916    // __vm-define-record-type: delegate to the tree-walker
917    let drt_env = env.clone();
918    env.set(
919        intern("__vm-define-record-type"),
920        Value::native_fn(NativeFn::with_ctx(
921            "__vm-define-record-type",
922            move |ctx, args| {
923                if args.len() != 5 {
924                    return Err(SemaError::arity("define-record-type", "5", args.len()));
925                }
926                let mut ctor_form = vec![args[1].clone()];
927                if let Some(fields) = args[3].as_list() {
928                    ctor_form.extend(fields.iter().cloned());
929                }
930                let mut form = vec![
931                    Value::symbol("define-record-type"),
932                    args[0].clone(),
933                    Value::list(ctor_form),
934                    args[2].clone(),
935                ];
936                if let Some(specs) = args[4].as_list() {
937                    for spec in specs.iter() {
938                        form.push(spec.clone());
939                    }
940                }
941                sema_core::eval_callback(ctx, &Value::list(form), &drt_env)
942            },
943        )),
944    );
945
946    // __vm-delay: create a thunk with unevaluated body
947    env.set(
948        intern("__vm-delay"),
949        Value::native_fn(NativeFn::simple("__vm-delay", |args| {
950            if args.len() != 1 {
951                return Err(SemaError::arity("delay", "1", args.len()));
952            }
953            // args[0] is the unevaluated body expression (passed as a quoted constant)
954            Ok(Value::thunk(Thunk {
955                body: args[0].clone(),
956                forced: RefCell::new(None),
957            }))
958        })),
959    );
960
961    // __vm-force: force a thunk
962    let force_env = env.clone();
963    env.set(
964        intern("__vm-force"),
965        Value::native_fn(NativeFn::with_ctx("__vm-force", move |ctx, args| {
966            if args.len() != 1 {
967                return Err(SemaError::arity("force", "1", args.len()));
968            }
969            if let Some(thunk) = args[0].as_thunk_rc() {
970                if let Some(val) = thunk.forced.borrow().as_ref() {
971                    return Ok(val.clone());
972                }
973                let val = if thunk.body.as_native_fn_rc().is_some()
974                    || thunk.body.as_lambda_rc().is_some()
975                {
976                    sema_core::call_callback(ctx, &thunk.body, &[])?
977                } else {
978                    sema_core::eval_callback(ctx, &thunk.body, &force_env)?
979                };
980                *thunk.forced.borrow_mut() = Some(val.clone());
981                Ok(val)
982            } else {
983                Ok(args[0].clone())
984            }
985        })),
986    );
987
988    // __vm-macroexpand: expand a macro form via the tree-walker
989    let me_env = env.clone();
990    env.set(
991        intern("__vm-macroexpand"),
992        Value::native_fn(NativeFn::with_ctx("__vm-macroexpand", move |ctx, args| {
993            if args.len() != 1 {
994                return Err(SemaError::arity("macroexpand", "1", args.len()));
995            }
996            if let Some(items) = args[0].as_list() {
997                if !items.is_empty() {
998                    if let Some(spur) = items[0].as_symbol_spur() {
999                        if let Some(mac_val) = me_env.get(spur) {
1000                            if let Some(mac) = mac_val.as_macro_rc() {
1001                                return apply_macro(ctx, &mac, &items[1..], &me_env);
1002                            }
1003                        }
1004                    }
1005                }
1006            }
1007            Ok(args[0].clone())
1008        })),
1009    );
1010
1011    // __vm-prompt: build Prompt directly from pre-evaluated entries
1012    env.set(
1013        intern("__vm-prompt"),
1014        Value::native_fn(NativeFn::simple("__vm-prompt", |args| {
1015            use sema_core::{Message, Prompt, Role};
1016            if args.len() != 1 {
1017                return Err(SemaError::arity("__vm-prompt", "1", args.len()));
1018            }
1019            let entries = args[0]
1020                .as_list()
1021                .ok_or_else(|| SemaError::type_error("list", args[0].type_name()))?;
1022            let mut messages = Vec::new();
1023            for entry in entries {
1024                if let Some(msg) = entry.as_message_rc() {
1025                    messages.push((*msg).clone());
1026                } else if let Some(pair) = entry.as_list() {
1027                    if pair.len() == 2 {
1028                        let role_str = pair[0]
1029                            .as_str()
1030                            .ok_or_else(|| SemaError::eval("prompt: expected role string"))?;
1031                        let role = match role_str {
1032                            "system" => Role::System,
1033                            "user" => Role::User,
1034                            "assistant" => Role::Assistant,
1035                            "tool" => Role::Tool,
1036                            other => {
1037                                return Err(SemaError::eval(format!(
1038                                    "prompt: unknown role '{other}'"
1039                                )))
1040                            }
1041                        };
1042                        let parts = pair[1]
1043                            .as_list()
1044                            .ok_or_else(|| SemaError::type_error("list", pair[1].type_name()))?;
1045                        let mut content = String::new();
1046                        for part in parts {
1047                            if let Some(s) = part.as_str() {
1048                                content.push_str(s);
1049                            } else {
1050                                content.push_str(&part.to_string());
1051                            }
1052                        }
1053                        messages.push(Message {
1054                            role,
1055                            content,
1056                            images: Vec::new(),
1057                        });
1058                    } else {
1059                        return Err(SemaError::eval(
1060                            "prompt: expected (role parts) pair or message value",
1061                        ));
1062                    }
1063                } else {
1064                    return Err(SemaError::eval(
1065                        "prompt: expected (role parts) pair or message value",
1066                    ));
1067                }
1068            }
1069            Ok(Value::prompt(Prompt { messages }))
1070        })),
1071    );
1072
1073    // __vm-message: build Message directly from pre-evaluated parts
1074    env.set(
1075        intern("__vm-message"),
1076        Value::native_fn(NativeFn::simple("__vm-message", |args| {
1077            use sema_core::{Message, Role};
1078            if args.len() != 2 {
1079                return Err(SemaError::arity("__vm-message", "2", args.len()));
1080            }
1081            let role = if let Some(spur) = args[0].as_keyword_spur() {
1082                let s = resolve(spur);
1083                match s.as_str() {
1084                    "system" => Role::System,
1085                    "user" => Role::User,
1086                    "assistant" => Role::Assistant,
1087                    "tool" => Role::Tool,
1088                    other => {
1089                        return Err(SemaError::eval(format!("message: unknown role '{other}'")))
1090                    }
1091                }
1092            } else {
1093                return Err(SemaError::type_error("keyword", args[0].type_name()));
1094            };
1095            let parts = args[1]
1096                .as_list()
1097                .ok_or_else(|| SemaError::type_error("list", args[1].type_name()))?;
1098            let mut content = String::new();
1099            for part in parts {
1100                if let Some(s) = part.as_str() {
1101                    content.push_str(s);
1102                } else {
1103                    content.push_str(&part.to_string());
1104                }
1105            }
1106            Ok(Value::message(Message {
1107                role,
1108                content,
1109                images: Vec::new(),
1110            }))
1111        })),
1112    );
1113
1114    // __vm-deftool: delegate to tree-walker
1115    let tool_env = env.clone();
1116    env.set(
1117        intern("__vm-deftool"),
1118        Value::native_fn(NativeFn::with_ctx("__vm-deftool", move |ctx, args| {
1119            if args.len() != 4 {
1120                return Err(SemaError::arity("deftool", "4", args.len()));
1121            }
1122            let form = Value::list(vec![
1123                Value::symbol("deftool"),
1124                args[0].clone(),
1125                args[1].clone(),
1126                args[2].clone(),
1127                args[3].clone(),
1128            ]);
1129            sema_core::eval_callback(ctx, &form, &tool_env)
1130        })),
1131    );
1132
1133    // __vm-defagent: delegate to tree-walker
1134    let agent_env = env.clone();
1135    env.set(
1136        intern("__vm-defagent"),
1137        Value::native_fn(NativeFn::with_ctx("__vm-defagent", move |ctx, args| {
1138            if args.len() != 2 {
1139                return Err(SemaError::arity("defagent", "2", args.len()));
1140            }
1141            let form = Value::list(vec![
1142                Value::symbol("defagent"),
1143                args[0].clone(),
1144                args[1].clone(),
1145            ]);
1146            sema_core::eval_callback(ctx, &form, &agent_env)
1147        })),
1148    );
1149
1150    // __vm-destructure: strict destructure — errors on shape mismatch
1151    // (pattern value) -> map of bindings keyed by symbol
1152    env.set(
1153        intern("__vm-destructure"),
1154        Value::native_fn(NativeFn::simple("__vm-destructure", |args| {
1155            if args.len() != 2 {
1156                return Err(SemaError::arity("__vm-destructure", "2", args.len()));
1157            }
1158            let bindings = crate::destructure::destructure(&args[0], &args[1])?;
1159            let mut map = std::collections::BTreeMap::new();
1160            for (spur, val) in bindings {
1161                map.insert(Value::symbol_from_spur(spur), val);
1162            }
1163            Ok(Value::map(map))
1164        })),
1165    );
1166
1167    // __vm-try-match: soft match — returns nil on no match, map of bindings on match
1168    // (pattern value) -> nil | map of bindings keyed by symbol
1169    env.set(
1170        intern("__vm-try-match"),
1171        Value::native_fn(NativeFn::simple("__vm-try-match", |args| {
1172            if args.len() != 2 {
1173                return Err(SemaError::arity("__vm-try-match", "2", args.len()));
1174            }
1175            match crate::destructure::try_match(&args[0], &args[1])? {
1176                Some(bindings) => {
1177                    let mut map = std::collections::BTreeMap::new();
1178                    for (spur, val) in bindings {
1179                        map.insert(Value::symbol_from_spur(spur), val);
1180                    }
1181                    Ok(Value::map(map))
1182                }
1183                None => Ok(Value::nil()),
1184            }
1185        })),
1186    );
1187
1188    // __vm-make-multi: create a MultiMethod value
1189    env.set(
1190        intern("__vm-make-multi"),
1191        Value::native_fn(NativeFn::simple("__vm-make-multi", |args| {
1192            if args.len() != 2 {
1193                return Err(SemaError::arity("__vm-make-multi", "2", args.len()));
1194            }
1195            let name_spur = args[0]
1196                .as_symbol_spur()
1197                .ok_or_else(|| SemaError::eval("__vm-make-multi: expected symbol"))?;
1198            Ok(Value::multimethod(MultiMethod {
1199                name: name_spur,
1200                dispatch_fn: args[1].clone(),
1201                methods: RefCell::new(std::collections::BTreeMap::new()),
1202                default: RefCell::new(None),
1203            }))
1204        })),
1205    );
1206
1207    // __vm-defmethod: add a method to an existing MultiMethod
1208    env.set(
1209        intern("__vm-defmethod"),
1210        Value::native_fn(NativeFn::simple("__vm-defmethod", |args| {
1211            if args.len() != 3 {
1212                return Err(SemaError::arity("__vm-defmethod", "3", args.len()));
1213            }
1214            let mm = args[0]
1215                .as_multimethod_rc()
1216                .ok_or_else(|| SemaError::eval("defmethod: first argument is not a multimethod"))?;
1217            let dispatch_val = &args[1];
1218            let handler = &args[2];
1219            if let Some(kw) = dispatch_val.as_keyword_spur() {
1220                if resolve(kw) == "default" {
1221                    *mm.default.borrow_mut() = Some(handler.clone());
1222                    return Ok(Value::nil());
1223                }
1224            }
1225            mm.methods
1226                .borrow_mut()
1227                .insert(dispatch_val.clone(), handler.clone());
1228            Ok(Value::nil())
1229        })),
1230    );
1231}