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        }
70        let global_env = Rc::new(env);
71        register_vm_delegates(&global_env);
72        load_prelude(&ctx, &global_env);
73        Interpreter { global_env, ctx }
74    }
75
76    pub fn new_with_sandbox(sandbox: &sema_core::Sandbox) -> Self {
77        let env = Env::new();
78        let ctx = EvalContext::new_with_sandbox(sandbox.clone());
79        sema_core::set_eval_callback(&ctx, eval_value_vm);
80        sema_core::set_call_callback(&ctx, call_value);
81        sema_stdlib::register_stdlib(&env, sandbox);
82        #[cfg(not(target_arch = "wasm32"))]
83        {
84            sema_llm::builtins::reset_runtime_state();
85            sema_llm::builtins::register_llm_builtins(&env, sandbox);
86        }
87        let global_env = Rc::new(env);
88        register_vm_delegates(&global_env);
89        load_prelude(&ctx, &global_env);
90        Interpreter { global_env, ctx }
91    }
92
93    /// Evaluate a single expression on the VM. M6: the VM is the sole evaluator.
94    ///
95    /// NOTE (deliberate behavior change vs. the retired tree-walker): all eval
96    /// entry points now run in the global env, so top-level `define`s persist
97    /// across calls. The old `eval`/`eval_str` child-env isolation is gone —
98    /// maintaining two env semantics was the dual-evaluator complexity being
99    /// removed. Use a fresh `Interpreter` for an isolated evaluation.
100    pub fn eval(&self, expr: &Value) -> EvalResult {
101        self.eval_in_global(expr)
102    }
103
104    /// Parse and evaluate on the VM (global env; `define`s persist — see `eval`).
105    pub fn eval_str(&self, input: &str) -> EvalResult {
106        self.eval_str_in_global(input)
107    }
108
109    /// Evaluate in the global environment so that `define` persists across calls.
110    pub fn eval_in_global(&self, expr: &Value) -> EvalResult {
111        self.run_exprs_on_vm(std::slice::from_ref(expr), &self.global_env)
112    }
113
114    /// Parse and evaluate in the global environment so that `define` persists across calls.
115    pub fn eval_str_in_global(&self, input: &str) -> EvalResult {
116        let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
117        self.ctx.merge_span_table(spans);
118        if exprs.is_empty() {
119            return Ok(Value::nil());
120        }
121        self.run_exprs_on_vm(&exprs, &self.global_env)
122    }
123
124    /// Parse, compile to bytecode, and execute via the VM (global env, persists).
125    pub fn eval_str_compiled(&self, input: &str) -> EvalResult {
126        let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
127        self.ctx.merge_span_table(spans);
128        if exprs.is_empty() {
129            return Ok(Value::nil());
130        }
131        self.run_exprs_on_vm(&exprs, &self.global_env)
132    }
133
134    /// Macro-expand, compile, and run a sequence of top-level forms on the VM,
135    /// rooted at `globals`. Shared by every eval entry point (M6: single
136    /// evaluator). `define`s land in `globals`.
137    fn run_exprs_on_vm(&self, exprs: &[Value], globals: &Rc<Env>) -> EvalResult {
138        let mut expanded = Vec::with_capacity(exprs.len());
139        for expr in exprs {
140            expanded.push(expand_for_vm_in(&self.ctx, globals, expr)?);
141        }
142        let known_natives = collect_native_names(globals);
143        let prog = sema_vm::compile_program(&expanded, Some(known_natives))?;
144        let mut vm = sema_vm::VM::new(
145            globals.clone(),
146            prog.functions,
147            &prog.native_table,
148            prog.main_cache_slots,
149        )?;
150        sema_vm::init_scheduler(self.global_env.clone(), prog.native_table.clone());
151        // Reset the loop-guard step counter so the limit (if any) is per top-level
152        // eval, not cumulative across calls on a reused interpreter.
153        self.ctx.eval_steps.set(0);
154        vm.execute(prog.closure, &self.ctx)
155    }
156
157    /// Compile source code to bytecode without executing.
158    /// Handles macro expansion (defmacro + macro calls) before compilation.
159    pub fn compile_to_bytecode(&self, input: &str) -> Result<sema_vm::CompileResult, SemaError> {
160        let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
161        self.ctx.merge_span_table(spans);
162
163        let mut expanded = Vec::new();
164        for expr in &exprs {
165            let exp = self.expand_for_vm(expr)?;
166            if !exp.is_nil() {
167                expanded.push(exp);
168            }
169        }
170
171        if expanded.is_empty() {
172            expanded.push(Value::nil());
173        }
174
175        let prog = sema_vm::compile_program(&expanded, None)?;
176        Ok(sema_vm::CompileResult::new(
177            prog.closure.func.chunk.clone(),
178            prog.functions.iter().map(|f| (**f).clone()).collect(),
179        ))
180    }
181
182    /// Pre-process a top-level expression for VM compilation: register any
183    /// `defmacro` forms, then expand macro calls in all other forms.
184    pub fn expand_for_vm(&self, expr: &Value) -> EvalResult {
185        expand_for_vm_in(&self.ctx, &self.global_env, expr)
186    }
187}
188
189/// Pre-process a top-level expression for VM compilation, expanding macro calls
190/// and eagerly registering `defmacro` forms — against `env` rather than a fixed
191/// global env. For top-level code `env` is the global env (unchanged behavior);
192/// for a `load`ed module body it is the same shared global env, so a `defmacro`
193/// registers where `expand_macros_in` looks it up and inherited macros still
194/// resolve via the parent chain.
195pub fn expand_for_vm_in(ctx: &EvalContext, env: &Env, expr: &Value) -> EvalResult {
196    if let Some(items) = expr.as_list() {
197        if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
198            let name = resolve(s);
199            if name == "defmacro" {
200                // Register the macro directly (pure destructure) — the VM macro
201                // path must not route through the tree-walker's `eval_value`.
202                register_defmacro(items, env)?;
203                return Ok(Value::nil());
204            }
205            if name == "begin" || name == "progn" {
206                let mut new_items = vec![Value::symbol_from_spur(s)];
207                let mut changed = false;
208                for item in &items[1..] {
209                    let expanded = expand_for_vm_in(ctx, env, item)?;
210                    if expanded.raw_bits() != item.raw_bits() {
211                        changed = true;
212                    }
213                    new_items.push(expanded);
214                }
215                if !changed {
216                    return Ok(expr.clone());
217                }
218                return Ok(Value::list(new_items));
219            }
220        }
221    }
222    expand_macros_in(ctx, env, expr)
223}
224
225/// Recursively expand macro calls, resolving macros via `env` (walking the
226/// parent chain). Preserves Rc pointer identity when no expansion occurs so span
227/// lookups (keyed by Rc pointer) remain valid.
228fn expand_macros_in(ctx: &EvalContext, env: &Env, expr: &Value) -> EvalResult {
229    if let Some(items) = expr.as_list() {
230        if !items.is_empty() {
231            if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
232                let name = resolve(s);
233                if name == "quote" {
234                    return Ok(expr.clone());
235                }
236                if let Some(mac_val) = env.get(s) {
237                    if let Some(mac) = mac_val.as_macro_rc() {
238                        // VM-native expansion: apply the transformer on the VM,
239                        // not the tree-walker.
240                        let expanded = apply_macro_vm(ctx, &mac, &items[1..], env)?;
241                        return expand_macros_in(ctx, env, &expanded);
242                    }
243                }
244            }
245            let expanded: Vec<Value> = items
246                .iter()
247                .map(|v| expand_macros_in(ctx, env, v))
248                .collect::<Result<_, _>>()?;
249            let changed = expanded
250                .iter()
251                .zip(items.iter())
252                .any(|(a, b)| a.raw_bits() != b.raw_bits());
253            if !changed {
254                return Ok(expr.clone());
255            }
256            return Ok(Value::list(expanded));
257        }
258    }
259    Ok(expr.clone())
260}
261
262/// Compile and run a `load`ed module body on the VM, one top-level form at a
263/// time so a `defmacro` / nested `load` that registers a macro is visible to
264/// later forms before they compile. `env` is the caller's shared global env, so
265/// defines land in the global scope (matching `load` semantics). Returns the
266/// value of the last form (nil for an empty body).
267///
268/// Only used for `load` (not `import`): `load` shares the global env, so module
269/// functions resolve their globals against the same env every VM uses — avoiding
270/// the per-module-globals problem that makes VM-backed `import` incorrect (see
271/// docs/plans/2026-06-16-vm-module-loading.md). Does NOT (re)initialize the async
272/// scheduler — it reuses the one installed by the top-level VM driver.
273pub fn eval_module_body_vm(
274    ctx: &EvalContext,
275    env: &Env,
276    exprs: &[Value],
277    span_map: &sema_core::SpanMap,
278    source_file: Option<std::path::PathBuf>,
279) -> EvalResult {
280    let mut result = Value::nil();
281    for expr in exprs {
282        let expanded = expand_for_vm_in(ctx, env, expr)?;
283        // `defmacro` (and forms that expand to nothing) are applied by expansion;
284        // there is nothing to compile/run for them.
285        if expanded.is_nil() {
286            continue;
287        }
288        let prog = sema_vm::compile_program_with_spans(
289            std::slice::from_ref(&expanded),
290            span_map,
291            source_file.clone(),
292        )?;
293        let globals = Rc::new(env.clone());
294        let mut vm = sema_vm::VM::new(
295            globals,
296            prog.functions,
297            &prog.native_table,
298            prog.main_cache_slots,
299        )?;
300        result = vm.execute(prog.closure, ctx)?;
301    }
302    // Each per-form VM ran on a clone of `env` with its own version cell, so any
303    // globals (re)defined by the body did not bump `env`'s version. Bump it now
304    // so the calling VM (whose globals share `env`'s bindings) invalidates its
305    // inline global cache and re-reads, rather than serving stale cached values.
306    env.bump_version();
307    Ok(result)
308}
309
310/// VM-native evaluation for callback consumers (e.g. sema-llm tool handlers):
311/// macro-expand, compile, and run `expr` on a fresh bytecode VM rooted at `env`.
312/// This is the VM-backed counterpart of `eval_value`, used to keep the
313/// eval-callback path off the tree-walker (M5 / Phase 1c). Each call builds a
314/// throwaway VM over a clone of `env` (sharing its bindings), so it is suited to
315/// one-shot evaluation rather than a persistent define-accumulating session.
316pub fn eval_value_vm(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
317    let env_rc = Rc::new(env.clone());
318    let expanded = expand_for_vm_in(ctx, &env_rc, expr)?;
319    if expanded.is_nil() {
320        return Ok(Value::nil());
321    }
322    let prog = sema_vm::compile_program(std::slice::from_ref(&expanded), None)?;
323    let mut vm = sema_vm::VM::new(env_rc, prog.functions, &[], prog.main_cache_slots)?;
324    vm.execute(prog.closure, ctx)
325}
326
327/// Call a function value with already-evaluated arguments.
328/// This is the public API for stdlib functions that need to invoke callbacks.
329///
330/// For lambdas, this delegates to `apply_lambda` + a trampoline loop so that
331/// subsequent evaluation happens iteratively rather than adding Rust stack
332/// frames.  This is critical for WASM where the call stack is limited (~5 MB).
333pub fn call_value(ctx: &EvalContext, func: &Value, args: &[Value]) -> EvalResult {
334    match func.view() {
335        ValueView::NativeFn(native) => (native.func)(ctx, args),
336        ValueView::Lambda(_) => {
337            // Raw `Lambda` values never occur on the VM path (user lambdas are
338            // NativeFn-wrapped VM closures); the tree-walker that produced them
339            // has been retired.
340            Err(SemaError::eval(
341                "internal: raw lambda value reached call_value (VM closures are native-fn-wrapped)"
342                    .to_string(),
343            ))
344        }
345        ValueView::Keyword(spur) => {
346            if args.len() != 1 {
347                let name = resolve(spur);
348                return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
349            }
350            let key = Value::keyword_from_spur(spur);
351            match args[0].view() {
352                ValueView::Map(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
353                ValueView::HashMap(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
354                _ => Err(SemaError::type_error_with_value(
355                    "map",
356                    args[0].type_name(),
357                    &args[0],
358                )),
359            }
360        }
361        ValueView::MultiMethod(mm) => call_multimethod(ctx, &mm, args),
362        _ => Err(
363            SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
364                .with_hint("expected a function, lambda, or keyword"),
365        ),
366    }
367}
368
369/// Call a multimethod: dispatch on args, look up handler, call it.
370fn call_multimethod(ctx: &EvalContext, mm: &Rc<MultiMethod>, args: &[Value]) -> EvalResult {
371    let dispatch_val = call_value(ctx, &mm.dispatch_fn, args)?;
372    let methods = mm.methods.borrow();
373    if let Some(handler) = methods.get(&dispatch_val) {
374        let handler = handler.clone();
375        drop(methods);
376        call_value(ctx, &handler, args)
377    } else {
378        drop(methods);
379        let default = mm.default.borrow().clone();
380        if let Some(handler) = default {
381            call_value(ctx, &handler, args)
382        } else {
383            Err(SemaError::eval(format!(
384                "no method in multimethod '{}' for dispatch value: {}",
385                resolve(mm.name),
386                dispatch_val
387            ))
388            .with_hint("add a (defmethod name :default handler) to handle unmatched values"))
389        }
390    }
391}
392
393/// Run a trampoline to completion iteratively.
394/// Used by `call_value` so that stdlib HOF callbacks (map, for-each, etc.)
395/// don't grow the Rust call stack for every evaluation step.
396/// Apply a macro by evaluating its body on the **bytecode VM** (no tree-walker).
397///
398/// This is the VM-native counterpart of [`apply_macro`]. The macro's
399/// (unevaluated) arguments are bound — together with a possible rest list — as
400/// *globals* in a transient child env of `caller_env`; the transformer body is
401/// then compiled fresh per call site (so auto-gensym stays hygienic — a cached
402/// transformer would reuse the same gensym across call sites) and run on a VM
403/// rooted at that env. Rooting at `caller_env` lets transformer bodies call
404/// global helpers and reference module-level bindings, and binding params as
405/// globals lets the compiled body resolve them via `GetGlobal`.
406///
407/// Used by the VM macro pre-expansion path (`expand_macros_in`) and
408/// `__vm-macroexpand`. The tree-walker's own lazy expansion keeps using
409/// [`apply_macro`] until the tree-walker is retired.
410pub fn apply_macro_vm(
411    ctx: &EvalContext,
412    mac: &sema_core::Macro,
413    args: &[Value],
414    caller_env: &Env,
415) -> Result<Value, SemaError> {
416    let env = Rc::new(Env::with_parent(Rc::new(caller_env.clone())));
417
418    // Bind parameters to unevaluated forms (same arity rules as apply_macro).
419    if let Some(rest) = mac.rest_param {
420        if args.len() < mac.params.len() {
421            return Err(SemaError::arity(
422                resolve(mac.name),
423                format!("{}+", mac.params.len()),
424                args.len(),
425            ));
426        }
427        for (param, arg) in mac.params.iter().zip(args.iter()) {
428            env.set(*param, arg.clone());
429        }
430        env.set(rest, Value::list(args[mac.params.len()..].to_vec()));
431    } else {
432        if args.len() != mac.params.len() {
433            return Err(SemaError::arity(
434                resolve(mac.name),
435                mac.params.len().to_string(),
436                args.len(),
437            ));
438        }
439        for (param, arg) in mac.params.iter().zip(args.iter()) {
440            env.set(*param, arg.clone());
441        }
442    }
443
444    // Compile and run each body form on the VM, fresh per call site (no cache)
445    // to keep auto-gensym hygienic. The body is the *transformer* code; it is
446    // NOT macro-pre-expanded here — quasiquote templates inside it (which may
447    // legitimately mention the macro's own name, as the recursive threading
448    // macros do) must be compiled as data, not re-expanded. Any macro call the
449    // transformer *produces* is re-expanded by the caller (`expand_macros_in`
450    // recurses on the returned form). `compile_program` lowers quasiquote /
451    // unquote / unquote-splicing directly, matching the tree-walker's
452    // `eval_value` over the same body.
453    let mut result = Value::nil();
454    for expr in &mac.body {
455        let prog = sema_vm::compile_program(std::slice::from_ref(expr), None)?;
456        let mut vm = sema_vm::VM::new(env.clone(), prog.functions, &[], prog.main_cache_slots)?;
457        result = vm.execute(prog.closure, ctx)?;
458    }
459    Ok(result)
460}
461
462/// Register a `defmacro` form's macro in `env` **without** the tree-walker — a
463/// pure destructure mirroring `special_forms::eval_defmacro`. Used by the VM
464/// pre-expansion path so registering a macro never routes through `eval_value`.
465fn register_defmacro(items: &[Value], env: &Env) -> Result<(), SemaError> {
466    // items[0] is the `defmacro` symbol; the rest are name, params, body…
467    let args = &items[1..];
468    if args.len() < 3 {
469        return Err(SemaError::arity("defmacro", "3+", args.len()));
470    }
471    let name_spur = args[0]
472        .as_symbol_spur()
473        .ok_or_else(|| SemaError::eval("defmacro: name must be a symbol"))?;
474    let param_list = args[1]
475        .as_list()
476        .ok_or_else(|| SemaError::eval("defmacro: params must be a list"))?;
477    let param_names: Vec<sema_core::Spur> = param_list
478        .iter()
479        .map(|v| {
480            v.as_symbol_spur()
481                .ok_or_else(|| SemaError::eval("defmacro: parameter must be a symbol"))
482        })
483        .collect::<Result<_, _>>()?;
484    let (params, rest_param) = special_forms::parse_params(&param_names);
485    let body = args[2..].to_vec();
486    env.set(
487        name_spur,
488        Value::macro_val(Macro {
489            params,
490            rest_param,
491            body,
492            name: name_spur,
493        }),
494    );
495    Ok(())
496}
497
498/// Register `__vm-*` native functions that the bytecode VM calls back into
499/// the tree-walker for forms that cannot be fully compiled.
500/// Load built-in macros (threading, when-let, if-let) into the global environment.
501pub fn load_prelude(ctx: &EvalContext, env: &Rc<Env>) {
502    let exprs = sema_reader::read_many(crate::prelude::PRELUDE)
503        .unwrap_or_else(|e| panic!("internal: prelude failed to parse: {e}"));
504    // The prelude is exclusively `defmacro` forms. Register them via the
505    // VM-native pre-expansion path so prelude loading never routes macro
506    // registration through the tree-walker's `eval_value`.
507    for expr in &exprs {
508        expand_for_vm_in(ctx, env, expr)
509            .unwrap_or_else(|e| panic!("internal: prelude failed to load: {e}"));
510    }
511}
512
513pub fn register_vm_delegates(env: &Rc<Env>) {
514    // __vm-eval: macro-expand, compile, and run the expression on the bytecode
515    // VM (rooted at the global env so top-level `define`s persist). The runtime
516    // `(eval ...)` meta path is thus VM-native — it no longer round-trips
517    // through the tree-walker's `eval_value` (M3 / Phase 1c).
518    let eval_env = env.clone();
519    env.set(
520        intern("__vm-eval"),
521        Value::native_fn(NativeFn::with_ctx("__vm-eval", move |ctx, args| {
522            if args.len() != 1 {
523                return Err(SemaError::arity("eval", "1", args.len()));
524            }
525            let expanded = expand_for_vm_in(ctx, &eval_env, &args[0])?;
526            // A form that expands to nothing (e.g. a `defmacro`) yields nil.
527            if expanded.is_nil() {
528                return Ok(Value::nil());
529            }
530            let prog = sema_vm::compile_program(std::slice::from_ref(&expanded), None)?;
531            let mut vm =
532                sema_vm::VM::new(eval_env.clone(), prog.functions, &[], prog.main_cache_slots)?;
533            vm.execute(prog.closure, ctx)
534        })),
535    );
536
537    // __vm-module-exports: register a `(module name (export ...) ...)` form's
538    // declared export list with the active module-load scope, so `import`
539    // restricts the copied bindings to exactly those names. Without this the VM
540    // exported every top-level binding (private helpers leaked). Mirrors the
541    // tree-walker's `set_module_exports` call in eval_module.
542    env.set(
543        intern("__vm-module-exports"),
544        Value::native_fn(NativeFn::with_ctx(
545            "__vm-module-exports",
546            move |ctx, args| {
547                if args.len() != 1 {
548                    return Err(SemaError::arity("module-exports", "1", args.len()));
549                }
550                let names: Vec<String> = match args[0].as_list() {
551                    Some(items) => items
552                        .iter()
553                        .map(|v| {
554                            v.as_symbol().map(|s| s.to_string()).ok_or_else(|| {
555                                SemaError::eval("module: export names must be symbols")
556                            })
557                        })
558                        .collect::<Result<_, _>>()?,
559                    None => return Err(SemaError::type_error("list", args[0].type_name())),
560                };
561                ctx.set_module_exports(names);
562                Ok(Value::nil())
563            },
564        )),
565    );
566
567    // __vm-load: call the load driver (special_forms::eval_load) directly, not
568    // through the tree-walker's eval_step dispatch. The driver handles VFS
569    // resolution, file path push/pop, caching, and runs the loaded body on the
570    // VM (M4). The path arrives already evaluated from the VM.
571    let load_env = env.clone();
572    env.set(
573        intern("__vm-load"),
574        Value::native_fn(NativeFn::with_ctx("__vm-load", move |ctx, args| {
575            if args.len() != 1 {
576                return Err(SemaError::arity("load", "1", args.len()));
577            }
578            // Target the *currently executing* VM's env (the module being run),
579            // falling back to the global env at top level, so a nested `load`
580            // adds definitions to the right module env — not always the globals.
581            let target = sema_vm::current_vm_globals().unwrap_or_else(|| load_env.clone());
582            match special_forms::eval_load(std::slice::from_ref(&args[0]), &target, ctx)? {
583                Trampoline::Value(v) => Ok(v),
584                Trampoline::Eval(..) => Ok(Value::nil()),
585            }
586        })),
587    );
588
589    // __vm-import: call the import driver (special_forms::eval_import) directly,
590    // not through the tree-walker's eval_step dispatch. Under the VM backend the
591    // driver compiles and runs the module body on the VM (M4). The path and
592    // selective-import symbols arrive already evaluated from the VM.
593    let import_env = env.clone();
594    env.set(
595        intern("__vm-import"),
596        Value::native_fn(NativeFn::with_ctx("__vm-import", move |ctx, args| {
597            if args.len() != 2 {
598                return Err(SemaError::arity("import", "2", args.len()));
599            }
600            ctx.sandbox.check(sema_core::Caps::FS_READ, "import")?;
601            let mut imp_args = vec![args[0].clone()];
602            if let Some(items) = args[1].as_list() {
603                imp_args.extend(items.iter().cloned());
604            }
605            // Copy exports into the *currently executing* VM's env (the module
606            // being run), falling back to the global env at top level. This keeps
607            // a nested module's imports private to that module instead of leaking
608            // into the global env (M4 nested-module isolation).
609            let target = sema_vm::current_vm_globals().unwrap_or_else(|| import_env.clone());
610            match special_forms::eval_import(&imp_args, &target, ctx)? {
611                Trampoline::Value(v) => Ok(v),
612                Trampoline::Eval(..) => Ok(Value::nil()),
613            }
614        })),
615    );
616
617    // __vm-defmacro: register a macro in the environment
618    let macro_env = env.clone();
619    env.set(
620        intern("__vm-defmacro"),
621        Value::native_fn(NativeFn::simple("__vm-defmacro", move |args| {
622            if args.len() != 4 {
623                return Err(SemaError::arity("defmacro", "4", args.len()));
624            }
625            let name = match args[0].as_symbol_spur() {
626                Some(s) => s,
627                None => return Err(SemaError::type_error("symbol", args[0].type_name())),
628            };
629            let params = match args[1].as_list() {
630                Some(items) => items
631                    .iter()
632                    .map(|v| match v.as_symbol_spur() {
633                        Some(s) => Ok(s),
634                        None => Err(SemaError::type_error("symbol", v.type_name())),
635                    })
636                    .collect::<Result<Vec<_>, _>>()?,
637                None => return Err(SemaError::type_error("list", args[1].type_name())),
638            };
639            let rest_param = if let Some(s) = args[2].as_symbol_spur() {
640                Some(s)
641            } else if args[2].is_nil() {
642                None
643            } else {
644                return Err(SemaError::type_error("symbol or nil", args[2].type_name()));
645            };
646            let body = vec![args[3].clone()];
647            macro_env.set(
648                name,
649                Value::macro_val(Macro {
650                    params,
651                    rest_param,
652                    body,
653                    name,
654                }),
655            );
656            Ok(Value::nil())
657        })),
658    );
659
660    // __vm-defmacro-form: register a complete `(defmacro ...)` form directly
661    // (pure destructure) — no tree-walker round-trip. Used for defmacro that
662    // reaches compilation (e.g. non-top-level) rather than expand-time
663    // registration.
664    let dmf_env = env.clone();
665    env.set(
666        intern("__vm-defmacro-form"),
667        Value::native_fn(NativeFn::simple("__vm-defmacro-form", move |args| {
668            if args.len() != 1 {
669                return Err(SemaError::arity("defmacro-form", "1", args.len()));
670            }
671            let items = args[0]
672                .as_list()
673                .ok_or_else(|| SemaError::type_error("list", args[0].type_name()))?;
674            register_defmacro(items, &dmf_env)?;
675            Ok(Value::nil())
676        })),
677    );
678
679    // __vm-define-record-type: delegate to the tree-walker
680    let drt_env = env.clone();
681    env.set(
682        intern("__vm-define-record-type"),
683        Value::native_fn(NativeFn::simple("__vm-define-record-type", move |args| {
684            if args.len() != 5 {
685                return Err(SemaError::arity("define-record-type", "5", args.len()));
686            }
687            // Build the `(define-record-type ...)` argument list (without the head
688            // symbol) and register the type directly via the pure destructure —
689            // no tree-walker round-trip. eval_define_record_type only sets native
690            // ctor/predicate/accessor fns in the env; it evaluates no user code.
691            let mut ctor_form = vec![args[1].clone()];
692            if let Some(fields) = args[3].as_list() {
693                ctor_form.extend(fields.iter().cloned());
694            }
695            let mut dr_args = vec![args[0].clone(), Value::list(ctor_form), args[2].clone()];
696            if let Some(specs) = args[4].as_list() {
697                for spec in specs.iter() {
698                    dr_args.push(spec.clone());
699                }
700            }
701            match special_forms::eval_define_record_type(&dr_args, &drt_env)? {
702                Trampoline::Value(v) => Ok(v),
703                Trampoline::Eval(..) => Ok(Value::nil()),
704            }
705        })),
706    );
707
708    // __vm-delay: create a thunk with unevaluated body
709    env.set(
710        intern("__vm-delay"),
711        Value::native_fn(NativeFn::simple("__vm-delay", |args| {
712            if args.len() != 1 {
713                return Err(SemaError::arity("delay", "1", args.len()));
714            }
715            // args[0] is the unevaluated body expression (passed as a quoted constant)
716            Ok(Value::thunk(Thunk {
717                body: args[0].clone(),
718                forced: RefCell::new(None),
719            }))
720        })),
721    );
722
723    // __vm-force: force a thunk
724    let force_env = env.clone();
725    env.set(
726        intern("__vm-force"),
727        Value::native_fn(NativeFn::with_ctx("__vm-force", move |ctx, args| {
728            if args.len() != 1 {
729                return Err(SemaError::arity("force", "1", args.len()));
730            }
731            if let Some(thunk) = args[0].as_thunk_rc() {
732                if let Some(val) = thunk.forced.borrow().as_ref() {
733                    return Ok(val.clone());
734                }
735                let val = if thunk.body.as_native_fn_rc().is_some()
736                    || thunk.body.as_lambda_rc().is_some()
737                {
738                    sema_core::call_callback(ctx, &thunk.body, &[])?
739                } else {
740                    // Non-callable thunk body (a raw expr) — evaluate on the VM.
741                    eval_value_vm(ctx, &thunk.body, &force_env)?
742                };
743                *thunk.forced.borrow_mut() = Some(val.clone());
744                Ok(val)
745            } else {
746                Err(SemaError::type_error("thunk", args[0].type_name())
747                    .with_hint("force: argument must be a (delay ...) or promise — non-promise values are an error"))
748            }
749        })),
750    );
751
752    // __vm-macroexpand: expand a macro form via the tree-walker
753    let me_env = env.clone();
754    env.set(
755        intern("__vm-macroexpand"),
756        Value::native_fn(NativeFn::with_ctx("__vm-macroexpand", move |ctx, args| {
757            if args.len() != 1 {
758                return Err(SemaError::arity("macroexpand", "1", args.len()));
759            }
760            if let Some(items) = args[0].as_list() {
761                if !items.is_empty() {
762                    if let Some(spur) = items[0].as_symbol_spur() {
763                        if let Some(mac_val) = me_env.get(spur) {
764                            if let Some(mac) = mac_val.as_macro_rc() {
765                                // VM-native: expand the transformer on the VM.
766                                return apply_macro_vm(ctx, &mac, &items[1..], &me_env);
767                            }
768                        }
769                    }
770                }
771            }
772            Ok(args[0].clone())
773        })),
774    );
775
776    // __vm-prompt: build Prompt directly from pre-evaluated entries
777    env.set(
778        intern("__vm-prompt"),
779        Value::native_fn(NativeFn::simple("__vm-prompt", |args| {
780            use sema_core::{Message, Prompt, Role};
781            if args.len() != 1 {
782                return Err(SemaError::arity("__vm-prompt", "1", args.len()));
783            }
784            let entries = args[0]
785                .as_list()
786                .ok_or_else(|| SemaError::type_error("list", args[0].type_name()))?;
787            let mut messages = Vec::new();
788            for entry in entries {
789                if let Some(msg) = entry.as_message_rc() {
790                    messages.push((*msg).clone());
791                } else if let Some(pair) = entry.as_list() {
792                    if pair.len() == 2 {
793                        let role_str = pair[0]
794                            .as_str()
795                            .ok_or_else(|| SemaError::eval("prompt: expected role string"))?;
796                        let role = match role_str {
797                            "system" => Role::System,
798                            "user" => Role::User,
799                            "assistant" => Role::Assistant,
800                            "tool" => Role::Tool,
801                            other => {
802                                return Err(SemaError::eval(format!(
803                                    "prompt: unknown role '{other}'"
804                                )))
805                            }
806                        };
807                        let parts = pair[1]
808                            .as_list()
809                            .ok_or_else(|| SemaError::type_error("list", pair[1].type_name()))?;
810                        let mut content = String::new();
811                        for part in parts {
812                            if let Some(s) = part.as_str() {
813                                content.push_str(s);
814                            } else {
815                                content.push_str(&part.to_string());
816                            }
817                        }
818                        messages.push(Message {
819                            role,
820                            content,
821                            images: Vec::new(),
822                        });
823                    } else {
824                        return Err(SemaError::eval(
825                            "prompt: expected (role parts) pair or message value",
826                        ));
827                    }
828                } else {
829                    return Err(SemaError::eval(
830                        "prompt: expected (role parts) pair or message value",
831                    ));
832                }
833            }
834            Ok(Value::prompt(Prompt { messages }))
835        })),
836    );
837
838    // __vm-message: build Message directly from pre-evaluated parts
839    env.set(
840        intern("__vm-message"),
841        Value::native_fn(NativeFn::simple("__vm-message", |args| {
842            use sema_core::{Message, Role};
843            if args.len() != 2 {
844                return Err(SemaError::arity("__vm-message", "2", args.len()));
845            }
846            let role = if let Some(spur) = args[0].as_keyword_spur() {
847                let s = resolve(spur);
848                match s.as_str() {
849                    "system" => Role::System,
850                    "user" => Role::User,
851                    "assistant" => Role::Assistant,
852                    "tool" => Role::Tool,
853                    other => {
854                        return Err(SemaError::eval(format!("message: unknown role '{other}'")))
855                    }
856                }
857            } else {
858                return Err(SemaError::type_error("keyword", args[0].type_name()));
859            };
860            let parts = args[1]
861                .as_list()
862                .ok_or_else(|| SemaError::type_error("list", args[1].type_name()))?;
863            let mut content = String::new();
864            for part in parts {
865                if let Some(s) = part.as_str() {
866                    content.push_str(s);
867                } else {
868                    content.push_str(&part.to_string());
869                }
870            }
871            Ok(Value::message(Message {
872                role,
873                content,
874                images: Vec::new(),
875            }))
876        })),
877    );
878
879    // __vm-deftool: delegate to tree-walker
880    // __vm-deftool: the VM has already evaluated description/parameters/handler
881    // and passes them as values, so build the tool directly — no tree-walker
882    // round-trip.
883    let tool_env = env.clone();
884    env.set(
885        intern("__vm-deftool"),
886        Value::native_fn(NativeFn::simple("__vm-deftool", move |args| {
887            if args.len() != 4 {
888                return Err(SemaError::arity("deftool", "4", args.len()));
889            }
890            let name = args[0]
891                .as_symbol()
892                .ok_or_else(|| SemaError::eval("deftool: name must be a symbol"))?;
893            special_forms::register_tool(
894                &name,
895                args[1].clone(),
896                args[2].clone(),
897                args[3].clone(),
898                &tool_env,
899            )
900        })),
901    );
902
903    // __vm-defagent: the VM has already evaluated the options map, so build the
904    // agent directly — no tree-walker round-trip.
905    let agent_env = env.clone();
906    env.set(
907        intern("__vm-defagent"),
908        Value::native_fn(NativeFn::simple("__vm-defagent", move |args| {
909            if args.len() != 2 {
910                return Err(SemaError::arity("defagent", "2", args.len()));
911            }
912            let name = args[0]
913                .as_symbol()
914                .ok_or_else(|| SemaError::eval("defagent: name must be a symbol"))?;
915            special_forms::register_agent(&name, args[1].clone(), &agent_env)
916        })),
917    );
918
919    // __vm-destructure: strict destructure — errors on shape mismatch
920    // (pattern value) -> map of bindings keyed by symbol
921    env.set(
922        intern("__vm-destructure"),
923        Value::native_fn(NativeFn::simple("__vm-destructure", |args| {
924            if args.len() != 2 {
925                return Err(SemaError::arity("__vm-destructure", "2", args.len()));
926            }
927            let bindings = crate::destructure::destructure(&args[0], &args[1])?;
928            let mut map = std::collections::BTreeMap::new();
929            for (spur, val) in bindings {
930                map.insert(Value::symbol_from_spur(spur), val);
931            }
932            Ok(Value::map(map))
933        })),
934    );
935
936    // __vm-try-match: soft match — returns nil on no match, map of bindings on match
937    // (pattern value) -> nil | map of bindings keyed by symbol
938    env.set(
939        intern("__vm-try-match"),
940        Value::native_fn(NativeFn::simple("__vm-try-match", |args| {
941            if args.len() != 2 {
942                return Err(SemaError::arity("__vm-try-match", "2", args.len()));
943            }
944            match crate::destructure::try_match(&args[0], &args[1])? {
945                Some(bindings) => {
946                    let mut map = std::collections::BTreeMap::new();
947                    for (spur, val) in bindings {
948                        map.insert(Value::symbol_from_spur(spur), val);
949                    }
950                    Ok(Value::map(map))
951                }
952                None => Ok(Value::nil()),
953            }
954        })),
955    );
956
957    // __vm-match-failed: the strict `(match ...)` no-clause-matched path. Always
958    // raises an :eval error carrying the unmatched value. `match*` never calls
959    // this (it returns nil instead).
960    env.set(
961        intern("__vm-match-failed"),
962        Value::native_fn(NativeFn::simple("__vm-match-failed", |args| {
963            let val = args.first().cloned().unwrap_or_else(Value::nil);
964            Err(
965                SemaError::eval(format!("match: no clause matched value: {val}")).with_hint(
966                    "add a catch-all `(_ ...)` clause, or use `match*` to return nil on no match",
967                ),
968            )
969        })),
970    );
971
972    // __vm-make-multi: create a MultiMethod value
973    env.set(
974        intern("__vm-make-multi"),
975        Value::native_fn(NativeFn::simple("__vm-make-multi", |args| {
976            if args.len() != 2 {
977                return Err(SemaError::arity("__vm-make-multi", "2", args.len()));
978            }
979            let name_spur = args[0]
980                .as_symbol_spur()
981                .ok_or_else(|| SemaError::eval("__vm-make-multi: expected symbol"))?;
982            Ok(Value::multimethod(MultiMethod {
983                name: name_spur,
984                dispatch_fn: args[1].clone(),
985                methods: RefCell::new(std::collections::BTreeMap::new()),
986                default: RefCell::new(None),
987            }))
988        })),
989    );
990
991    // __vm-defmethod: add a method to an existing MultiMethod
992    env.set(
993        intern("__vm-defmethod"),
994        Value::native_fn(NativeFn::simple("__vm-defmethod", |args| {
995            if args.len() != 3 {
996                return Err(SemaError::arity("__vm-defmethod", "3", args.len()));
997            }
998            let mm = args[0]
999                .as_multimethod_rc()
1000                .ok_or_else(|| SemaError::eval("defmethod: first argument is not a multimethod"))?;
1001            let dispatch_val = &args[1];
1002            let handler = &args[2];
1003            if let Some(kw) = dispatch_val.as_keyword_spur() {
1004                if resolve(kw) == "default" {
1005                    *mm.default.borrow_mut() = Some(handler.clone());
1006                    return Ok(Value::nil());
1007                }
1008            }
1009            mm.methods
1010                .borrow_mut()
1011                .insert(dispatch_val.clone(), handler.clone());
1012            Ok(Value::nil())
1013        })),
1014    );
1015}