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, Env, EvalContext, Macro, MultiMethod, NativeFn, SemaError, Spur, Thunk, Value,
7    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/// Collect the names of all native functions in an environment.
35/// Used to tell the bytecode compiler which globals can use CallNative.
36fn 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
43/// The interpreter holds the global environment and state.
44pub 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        // Register eval/call callbacks so stdlib can invoke the real evaluator
60        sema_core::set_eval_callback(&ctx, eval_value_vm);
61        sema_core::set_call_callback(&ctx, call_value);
62        // Register stdlib
63        sema_stdlib::register_stdlib(&env, &sema_core::Sandbox::allow_all());
64        // Register LLM builtins
65        #[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    /// Evaluate a single expression on the VM. M6: the VM is the sole evaluator.
96    ///
97    /// NOTE (deliberate behavior change vs. the retired tree-walker): all eval
98    /// entry points now run in the global env, so top-level `define`s persist
99    /// across calls. The old `eval`/`eval_str` child-env isolation is gone —
100    /// maintaining two env semantics was the dual-evaluator complexity being
101    /// removed. Use a fresh `Interpreter` for an isolated evaluation.
102    pub fn eval(&self, expr: &Value) -> EvalResult {
103        self.eval_in_global(expr)
104    }
105
106    /// Parse and evaluate on the VM (global env; `define`s persist — see `eval`).
107    pub fn eval_str(&self, input: &str) -> EvalResult {
108        self.eval_str_in_global(input)
109    }
110
111    /// Evaluate in the global environment so that `define` persists across calls.
112    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    /// Parse and evaluate in the global environment so that `define` persists across calls.
117    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    /// Parse, compile to bytecode, and execute via the VM (global env, persists).
127    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    /// Macro-expand, compile, and run a sequence of top-level forms on the VM,
137    /// rooted at `globals`. Shared by every eval entry point (M6: single
138    /// evaluator). `define`s land in `globals`.
139    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        // Reset the loop-guard step counter so the limit (if any) is per top-level
154        // eval, not cumulative across calls on a reused interpreter.
155        self.ctx.eval_steps.set(0);
156        vm.execute(prog.closure, &self.ctx)
157    }
158
159    /// Compile source code to bytecode without executing.
160    /// Handles macro expansion (defmacro + macro calls) before compilation.
161    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    /// Pre-process a top-level expression for VM compilation: register any
185    /// `defmacro` forms, then expand macro calls in all other forms.
186    pub fn expand_for_vm(&self, expr: &Value) -> EvalResult {
187        expand_for_vm_in(&self.ctx, &self.global_env, expr)
188    }
189}
190
191/// Pre-process a top-level expression for VM compilation, expanding macro calls
192/// and eagerly registering `defmacro` forms — against `env` rather than a fixed
193/// global env. For top-level code `env` is the global env (unchanged behavior);
194/// for a `load`ed module body it is the same shared global env, so a `defmacro`
195/// registers where `expand_macros_in` looks it up and inherited macros still
196/// resolve via the parent chain.
197pub 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 the macro directly (pure destructure) — the VM macro
203                // path must not route through the tree-walker's `eval_value`.
204                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
227/// Recursively expand macro calls, resolving macros via `env` (walking the
228/// parent chain). Preserves Rc pointer identity when no expansion occurs so span
229/// lookups (keyed by Rc pointer) remain valid.
230fn 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                        // VM-native expansion: apply the transformer on the VM,
241                        // not the tree-walker.
242                        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
264/// Compile and run a `load`ed module body on the VM, one top-level form at a
265/// time so a `defmacro` / nested `load` that registers a macro is visible to
266/// later forms before they compile. `env` is the caller's shared global env, so
267/// defines land in the global scope (matching `load` semantics). Returns the
268/// value of the last form (nil for an empty body).
269///
270/// Only used for `load` (not `import`): `load` shares the global env, so module
271/// functions resolve their globals against the same env every VM uses — avoiding
272/// the per-module-globals problem that makes VM-backed `import` incorrect (see
273/// docs/plans/2026-06-16-vm-module-loading.md). Does NOT (re)initialize the async
274/// scheduler — it reuses the one installed by the top-level VM driver.
275pub 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        // `defmacro` (and forms that expand to nothing) are applied by expansion;
286        // there is nothing to compile/run for them.
287        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    // Each per-form VM ran on a clone of `env` with its own version cell, so any
305    // globals (re)defined by the body did not bump `env`'s version. Bump it now
306    // so the calling VM (whose globals share `env`'s bindings) invalidates its
307    // inline global cache and re-reads, rather than serving stale cached values.
308    env.bump_version();
309    Ok(result)
310}
311
312/// VM-native evaluation for callback consumers (e.g. sema-llm tool handlers):
313/// macro-expand, compile, and run `expr` on a fresh bytecode VM rooted at `env`.
314/// This is the VM-backed counterpart of `eval_value`, used to keep the
315/// eval-callback path off the tree-walker (M5 / Phase 1c). Each call builds a
316/// throwaway VM over a clone of `env` (sharing its bindings), so it is suited to
317/// one-shot evaluation rather than a persistent define-accumulating session.
318pub 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
329/// Call a function value with already-evaluated arguments.
330/// This is the public API for stdlib functions that need to invoke callbacks.
331///
332/// For lambdas, this delegates to `apply_lambda` + a trampoline loop so that
333/// subsequent evaluation happens iteratively rather than adding Rust stack
334/// frames.  This is critical for WASM where the call stack is limited (~5 MB).
335pub 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            // Raw `Lambda` values never occur on the VM path (user lambdas are
340            // NativeFn-wrapped VM closures); the tree-walker that produced them
341            // has been retired.
342            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
371/// Call a multimethod: dispatch on args, look up handler, call it.
372fn 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
395/// Run a trampoline to completion iteratively.
396/// Used by `call_value` so that stdlib HOF callbacks (map, for-each, etc.)
397/// don't grow the Rust call stack for every evaluation step.
398/// Apply a macro by evaluating its body on the **bytecode VM** (no tree-walker).
399///
400/// This is the VM-native counterpart of [`apply_macro`]. The macro's
401/// (unevaluated) arguments are bound — together with a possible rest list — as
402/// *globals* in a transient child env of `caller_env`; the transformer body is
403/// then compiled fresh per call site (so auto-gensym stays hygienic — a cached
404/// transformer would reuse the same gensym across call sites) and run on a VM
405/// rooted at that env. Rooting at `caller_env` lets transformer bodies call
406/// global helpers and reference module-level bindings, and binding params as
407/// globals lets the compiled body resolve them via `GetGlobal`.
408///
409/// Used by the VM macro pre-expansion path (`expand_macros_in`) and
410/// `__vm-macroexpand`. The tree-walker's own lazy expansion keeps using
411/// [`apply_macro`] until the tree-walker is retired.
412pub 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    // Bind parameters to unevaluated forms (same arity rules as apply_macro).
421    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    // Compile and run each body form on the VM, fresh per call site (no cache)
447    // to keep auto-gensym hygienic. The body is the *transformer* code; it is
448    // NOT macro-pre-expanded here — quasiquote templates inside it (which may
449    // legitimately mention the macro's own name, as the recursive threading
450    // macros do) must be compiled as data, not re-expanded. Any macro call the
451    // transformer *produces* is re-expanded by the caller (`expand_macros_in`
452    // recurses on the returned form). `compile_program` lowers quasiquote /
453    // unquote / unquote-splicing directly, matching the tree-walker's
454    // `eval_value` over the same body.
455    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
464/// Register a `defmacro` form's macro in `env` **without** the tree-walker — a
465/// pure destructure mirroring `special_forms::eval_defmacro`. Used by the VM
466/// pre-expansion path so registering a macro never routes through `eval_value`.
467fn register_defmacro(items: &[Value], env: &Env) -> Result<(), SemaError> {
468    // items[0] is the `defmacro` symbol; the rest are name, params, body…
469    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(&param_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
500/// Register `__vm-*` native functions that the bytecode VM calls back into
501/// the tree-walker for forms that cannot be fully compiled.
502/// Load built-in macros (threading, when-let, if-let) into the global environment.
503pub 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    // The prelude is exclusively `defmacro` forms. Register them via the
507    // VM-native pre-expansion path so prelude loading never routes macro
508    // registration through the tree-walker's `eval_value`.
509    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    // __vm-eval: macro-expand, compile, and run the expression on the bytecode
517    // VM (rooted at the global env so top-level `define`s persist). The runtime
518    // `(eval ...)` meta path is thus VM-native — it no longer round-trips
519    // through the tree-walker's `eval_value` (M3 / Phase 1c).
520    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            // A form that expands to nothing (e.g. a `defmacro`) yields nil.
529            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    // __vm-module-exports: register a `(module name (export ...) ...)` form's
540    // declared export list with the active module-load scope, so `import`
541    // restricts the copied bindings to exactly those names. Without this the VM
542    // exported every top-level binding (private helpers leaked). Mirrors the
543    // tree-walker's `set_module_exports` call in eval_module.
544    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    // __vm-load: call the load driver (special_forms::eval_load) directly, not
570    // through the tree-walker's eval_step dispatch. The driver handles VFS
571    // resolution, file path push/pop, caching, and runs the loaded body on the
572    // VM (M4). The path arrives already evaluated from the VM.
573    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            // Target the *currently executing* VM's env (the module being run),
581            // falling back to the global env at top level, so a nested `load`
582            // adds definitions to the right module env — not always the globals.
583            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    // __vm-import: call the import driver (special_forms::eval_import) directly,
592    // not through the tree-walker's eval_step dispatch. Under the VM backend the
593    // driver compiles and runs the module body on the VM (M4). The path and
594    // selective-import symbols arrive already evaluated from the VM.
595    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            // Copy exports into the *currently executing* VM's env (the module
608            // being run), falling back to the global env at top level. This keeps
609            // a nested module's imports private to that module instead of leaking
610            // into the global env (M4 nested-module isolation).
611            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    // __vm-defmacro: register a macro in the environment
620    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    // __vm-defmacro-form: register a complete `(defmacro ...)` form directly
663    // (pure destructure) — no tree-walker round-trip. Used for defmacro that
664    // reaches compilation (e.g. non-top-level) rather than expand-time
665    // registration.
666    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    // __vm-define-record-type: delegate to the tree-walker
682    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            // Build the `(define-record-type ...)` argument list (without the head
690            // symbol) and register the type directly via the pure destructure —
691            // no tree-walker round-trip. eval_define_record_type only sets native
692            // ctor/predicate/accessor fns in the env; it evaluates no user code.
693            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    // __vm-delay: create a thunk with unevaluated body
711    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            // args[0] is the unevaluated body expression (passed as a quoted constant)
718            Ok(Value::thunk(Thunk {
719                body: args[0].clone(),
720                forced: RefCell::new(None),
721            }))
722        })),
723    );
724
725    // __vm-force: force a thunk
726    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                    // Non-callable thunk body (a raw expr) — evaluate on the VM.
743                    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    // __vm-macroexpand: expand a macro form via the tree-walker
755    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                                // VM-native: expand the transformer on the VM.
768                                return apply_macro_vm(ctx, &mac, &items[1..], &me_env);
769                            }
770                        }
771                    }
772                }
773            }
774            Ok(args[0].clone())
775        })),
776    );
777
778    // __vm-prompt: build Prompt directly from pre-evaluated entries
779    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    // __vm-message: build Message directly from pre-evaluated parts
841    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    // __vm-deftool: delegate to tree-walker
882    // __vm-deftool: the VM has already evaluated description/parameters/handler
883    // and passes them as values, so build the tool directly — no tree-walker
884    // round-trip.
885    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    // __vm-defagent: the VM has already evaluated the options map, so build the
906    // agent directly — no tree-walker round-trip.
907    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    // __vm-destructure: strict destructure — errors on shape mismatch
922    // (pattern value) -> map of bindings keyed by symbol
923    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    // __vm-try-match: soft match — returns nil on no match, map of bindings on match
939    // (pattern value) -> nil | map of bindings keyed by symbol
940    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    // __vm-make-multi: create a MultiMethod value
960    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    // __vm-defmethod: add a method to an existing MultiMethod
979    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}