Skip to main content

sema_eval/
eval.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use sema_core::{
5    intern, resolve, CallFrame, Env, EvalContext, Lambda, Macro, NativeFn, SemaError, Span, Thunk,
6    Value, ValueView,
7};
8
9use crate::special_forms;
10
11/// Trampoline for tail-call optimization.
12pub enum Trampoline {
13    Value(Value),
14    Eval(Value, Env),
15}
16
17pub type EvalResult = Result<Value, SemaError>;
18
19/// Create an isolated module env: child of root (global/stdlib) env
20pub fn create_module_env(env: &Env) -> Env {
21    // Walk parent chain to find root
22    let mut current = env.clone();
23    loop {
24        let parent = current.parent.clone();
25        match parent {
26            Some(p) => current = (*p).clone(),
27            None => break,
28        }
29    }
30    Env::with_parent(Rc::new(current))
31}
32
33/// Look up a span for an expression via the span table in the context.
34fn span_of_expr(ctx: &EvalContext, expr: &Value) -> Option<Span> {
35    if let Some(items) = expr.as_list_rc() {
36        let ptr = Rc::as_ptr(&items) as usize;
37        ctx.lookup_span(ptr)
38    } else {
39        None
40    }
41}
42
43/// RAII guard that truncates the call stack on drop.
44struct CallStackGuard<'a> {
45    ctx: &'a EvalContext,
46    entry_depth: usize,
47}
48
49impl Drop for CallStackGuard<'_> {
50    fn drop(&mut self) {
51        self.ctx.truncate_call_stack(self.entry_depth);
52    }
53}
54
55/// The interpreter holds the global environment and state.
56pub struct Interpreter {
57    pub global_env: Rc<Env>,
58    pub ctx: EvalContext,
59}
60
61impl Default for Interpreter {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl Interpreter {
68    pub fn new() -> Self {
69        let env = Env::new();
70        let ctx = EvalContext::new();
71        // Register eval/call callbacks so stdlib can invoke the real evaluator
72        sema_core::set_eval_callback(&ctx, eval_value);
73        sema_core::set_call_callback(&ctx, call_value);
74        // Register stdlib
75        sema_stdlib::register_stdlib(&env, &sema_core::Sandbox::allow_all());
76        // Register LLM builtins
77        #[cfg(not(target_arch = "wasm32"))]
78        {
79            sema_llm::builtins::reset_runtime_state();
80            sema_llm::builtins::register_llm_builtins(&env, &sema_core::Sandbox::allow_all());
81            sema_llm::builtins::set_eval_callback(eval_value);
82        }
83        let global_env = Rc::new(env);
84        register_vm_delegates(&global_env);
85        Interpreter { global_env, ctx }
86    }
87
88    pub fn new_with_sandbox(sandbox: &sema_core::Sandbox) -> Self {
89        let env = Env::new();
90        let ctx = EvalContext::new_with_sandbox(sandbox.clone());
91        sema_core::set_eval_callback(&ctx, eval_value);
92        sema_core::set_call_callback(&ctx, call_value);
93        sema_stdlib::register_stdlib(&env, sandbox);
94        #[cfg(not(target_arch = "wasm32"))]
95        {
96            sema_llm::builtins::reset_runtime_state();
97            sema_llm::builtins::register_llm_builtins(&env, sandbox);
98            sema_llm::builtins::set_eval_callback(eval_value);
99        }
100        let global_env = Rc::new(env);
101        register_vm_delegates(&global_env);
102        Interpreter { global_env, ctx }
103    }
104
105    pub fn eval(&self, expr: &Value) -> EvalResult {
106        eval_value(&self.ctx, expr, &Env::with_parent(self.global_env.clone()))
107    }
108
109    pub fn eval_str(&self, input: &str) -> EvalResult {
110        eval_string(&self.ctx, input, &Env::with_parent(self.global_env.clone()))
111    }
112
113    /// Evaluate in the global environment so that `define` persists across calls.
114    pub fn eval_in_global(&self, expr: &Value) -> EvalResult {
115        eval_value(&self.ctx, expr, &self.global_env)
116    }
117
118    /// Parse and evaluate in the global environment so that `define` persists across calls.
119    pub fn eval_str_in_global(&self, input: &str) -> EvalResult {
120        eval_string(&self.ctx, input, &self.global_env)
121    }
122
123    /// Parse, compile to bytecode, and execute via the VM.
124    pub fn eval_str_compiled(&self, input: &str) -> EvalResult {
125        let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
126        self.ctx.merge_span_table(spans);
127        if exprs.is_empty() {
128            return Ok(Value::nil());
129        }
130
131        let mut expanded = Vec::new();
132        for expr in &exprs {
133            let exp = self.expand_for_vm(expr)?;
134            expanded.push(exp);
135        }
136
137        let (closure, functions) = sema_vm::compile_program(&expanded)?;
138        let mut vm = sema_vm::VM::new(self.global_env.clone(), functions);
139        vm.execute(closure, &self.ctx)
140    }
141
142    /// Compile a pre-parsed Value AST to bytecode and execute via the VM.
143    pub fn eval_compiled(&self, expr: &Value) -> EvalResult {
144        let expanded = self.expand_for_vm(expr)?;
145        let (closure, functions) = sema_vm::compile_program(std::slice::from_ref(&expanded))?;
146        let mut vm = sema_vm::VM::new(self.global_env.clone(), functions);
147        vm.execute(closure, &self.ctx)
148    }
149
150    /// Compile source code to bytecode without executing.
151    /// Handles macro expansion (defmacro + macro calls) before compilation.
152    pub fn compile_to_bytecode(&self, input: &str) -> Result<sema_vm::CompileResult, SemaError> {
153        let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
154        self.ctx.merge_span_table(spans);
155
156        let mut expanded = Vec::new();
157        for expr in &exprs {
158            let exp = self.expand_for_vm(expr)?;
159            if !exp.is_nil() {
160                expanded.push(exp);
161            }
162        }
163
164        if expanded.is_empty() {
165            expanded.push(Value::nil());
166        }
167
168        let (closure, functions) = sema_vm::compile_program(&expanded)?;
169        Ok(sema_vm::CompileResult {
170            chunk: closure.func.chunk.clone(),
171            functions: functions.iter().map(|f| (**f).clone()).collect(),
172        })
173    }
174
175    /// Pre-process a top-level expression for VM compilation.
176    /// Evaluates `defmacro` forms via the tree-walker to register macros,
177    /// then expands macro calls in all other forms.
178    fn expand_for_vm(&self, expr: &Value) -> EvalResult {
179        if let Some(items) = expr.as_list() {
180            if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
181                let name = resolve(s);
182                if name == "defmacro" {
183                    eval_value(&self.ctx, expr, &self.global_env)?;
184                    return Ok(Value::nil());
185                }
186                if name == "begin" {
187                    let mut new_items = vec![Value::symbol_from_spur(s)];
188                    for item in &items[1..] {
189                        new_items.push(self.expand_for_vm(item)?);
190                    }
191                    return Ok(Value::list(new_items));
192                }
193            }
194        }
195        self.expand_macros(expr)
196    }
197
198    /// Recursively expand macro calls in an expression.
199    fn expand_macros(&self, expr: &Value) -> EvalResult {
200        if let Some(items) = expr.as_list() {
201            if !items.is_empty() {
202                if let Some(s) = items.first().and_then(|v| v.as_symbol_spur()) {
203                    let name = resolve(s);
204                    if name == "quote" {
205                        return Ok(expr.clone());
206                    }
207                    if let Some(mac_val) = self.global_env.get(s) {
208                        if let Some(mac) = mac_val.as_macro_rc() {
209                            let expanded =
210                                apply_macro(&self.ctx, &mac, &items[1..], &self.global_env)?;
211                            return self.expand_macros(&expanded);
212                        }
213                    }
214                }
215                let expanded: Result<Vec<Value>, SemaError> =
216                    items.iter().map(|v| self.expand_macros(v)).collect();
217                return Ok(Value::list(expanded?));
218            }
219        }
220        Ok(expr.clone())
221    }
222}
223
224/// Evaluate a string containing one or more expressions.
225pub fn eval_string(ctx: &EvalContext, input: &str, env: &Env) -> EvalResult {
226    let (exprs, spans) = sema_reader::read_many_with_spans(input)?;
227    ctx.merge_span_table(spans);
228    let mut result = Value::nil();
229    for expr in &exprs {
230        result = eval_value(ctx, expr, env)?;
231    }
232    Ok(result)
233}
234
235/// The core eval function: evaluate a Value in an environment.
236pub fn eval(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
237    eval_value(ctx, expr, env)
238}
239
240/// Maximum eval nesting depth before we bail with an error.
241/// This prevents native stack overflow from unbounded recursion
242/// (both function calls and special form nesting like deeply nested if/let/begin).
243/// WASM has a much smaller call stack (~1MB V8 limit) so we use a lower depth.
244#[cfg(target_arch = "wasm32")]
245const MAX_EVAL_DEPTH: usize = 256;
246#[cfg(not(target_arch = "wasm32"))]
247const MAX_EVAL_DEPTH: usize = 1024;
248
249pub fn eval_value(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
250    // Fast path: self-evaluating forms skip depth/step tracking entirely.
251    match expr.view() {
252        ValueView::Nil
253        | ValueView::Bool(_)
254        | ValueView::Int(_)
255        | ValueView::Float(_)
256        | ValueView::String(_)
257        | ValueView::Char(_)
258        | ValueView::Keyword(_)
259        | ValueView::Thunk(_)
260        | ValueView::Bytevector(_)
261        | ValueView::NativeFn(_)
262        | ValueView::Lambda(_)
263        | ValueView::HashMap(_) => return Ok(expr.clone()),
264        ValueView::Symbol(spur) => {
265            if let Some(val) = env.get(spur) {
266                return Ok(val);
267            }
268            return Err(SemaError::Unbound(resolve(spur)));
269        }
270        _ => {}
271    }
272
273    let depth = ctx.eval_depth.get();
274    ctx.eval_depth.set(depth + 1);
275    if depth == 0 {
276        ctx.eval_steps.set(0);
277    }
278    if depth > MAX_EVAL_DEPTH {
279        ctx.eval_depth.set(ctx.eval_depth.get().saturating_sub(1));
280        return Err(SemaError::eval(format!(
281            "maximum eval depth exceeded ({MAX_EVAL_DEPTH})"
282        )));
283    }
284
285    let result = eval_value_inner(ctx, expr, env);
286
287    ctx.eval_depth.set(ctx.eval_depth.get().saturating_sub(1));
288    result
289}
290
291/// Call a function value with already-evaluated arguments.
292/// This is the public API for stdlib functions that need to invoke callbacks.
293pub fn call_value(ctx: &EvalContext, func: &Value, args: &[Value]) -> EvalResult {
294    match func.view() {
295        ValueView::NativeFn(native) => (native.func)(ctx, args),
296        ValueView::Lambda(lambda) => {
297            let new_env = Env::with_parent(Rc::new(lambda.env.clone()));
298
299            if let Some(rest) = lambda.rest_param {
300                if args.len() < lambda.params.len() {
301                    return Err(SemaError::arity(
302                        lambda
303                            .name
304                            .map(resolve)
305                            .unwrap_or_else(|| "lambda".to_string()),
306                        format!("{}+", lambda.params.len()),
307                        args.len(),
308                    ));
309                }
310                for (param, arg) in lambda.params.iter().zip(args.iter()) {
311                    new_env.set(*param, arg.clone());
312                }
313                let rest_args = args[lambda.params.len()..].to_vec();
314                new_env.set(rest, Value::list(rest_args));
315            } else {
316                if args.len() != lambda.params.len() {
317                    return Err(SemaError::arity(
318                        lambda
319                            .name
320                            .map(resolve)
321                            .unwrap_or_else(|| "lambda".to_string()),
322                        lambda.params.len().to_string(),
323                        args.len(),
324                    ));
325                }
326                for (param, arg) in lambda.params.iter().zip(args.iter()) {
327                    new_env.set(*param, arg.clone());
328                }
329            }
330
331            if let Some(name) = lambda.name {
332                new_env.set(name, Value::lambda_from_rc(Rc::clone(&lambda)));
333            }
334
335            let mut result = Value::nil();
336            for expr in &lambda.body {
337                result = eval_value(ctx, expr, &new_env)?;
338            }
339            Ok(result)
340        }
341        ValueView::Keyword(spur) => {
342            if args.len() != 1 {
343                let name = resolve(spur);
344                return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
345            }
346            let key = Value::keyword_from_spur(spur);
347            match args[0].view() {
348                ValueView::Map(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
349                ValueView::HashMap(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
350                _ => Err(SemaError::type_error("map", args[0].type_name())),
351            }
352        }
353        _ => Err(
354            SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
355                .with_hint("expected a function, lambda, or keyword"),
356        ),
357    }
358}
359
360fn eval_value_inner(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
361    let entry_depth = ctx.call_stack_depth();
362    let guard = CallStackGuard { ctx, entry_depth };
363    let limit = ctx.eval_step_limit.get();
364
365    // First iteration: use borrowed expr/env to avoid cloning
366    if limit > 0 {
367        let v = ctx.eval_steps.get() + 1;
368        ctx.eval_steps.set(v);
369        if v > limit {
370            return Err(SemaError::eval("eval step limit exceeded".to_string()));
371        }
372    }
373
374    match eval_step(ctx, expr, env) {
375        Ok(Trampoline::Value(v)) => {
376            drop(guard);
377            Ok(v)
378        }
379        Ok(Trampoline::Eval(next_expr, next_env)) => {
380            // Need to continue — enter the trampoline loop
381            let mut current_expr = next_expr;
382            let mut current_env = next_env;
383
384            // Trim call stack for TCO
385            {
386                let mut stack = ctx.call_stack.borrow_mut();
387                if stack.len() > entry_depth + 1 {
388                    let top = stack.last().cloned();
389                    stack.truncate(entry_depth);
390                    if let Some(frame) = top {
391                        stack.push(frame);
392                    }
393                }
394            }
395
396            loop {
397                if limit > 0 {
398                    let v = ctx.eval_steps.get() + 1;
399                    ctx.eval_steps.set(v);
400                    if v > limit {
401                        return Err(SemaError::eval("eval step limit exceeded".to_string()));
402                    }
403                }
404
405                match eval_step(ctx, &current_expr, &current_env) {
406                    Ok(Trampoline::Value(v)) => {
407                        drop(guard);
408                        return Ok(v);
409                    }
410                    Ok(Trampoline::Eval(next_expr, next_env)) => {
411                        {
412                            let mut stack = ctx.call_stack.borrow_mut();
413                            if stack.len() > entry_depth + 1 {
414                                let top = stack.last().cloned();
415                                stack.truncate(entry_depth);
416                                if let Some(frame) = top {
417                                    stack.push(frame);
418                                }
419                            }
420                        }
421                        current_expr = next_expr;
422                        current_env = next_env;
423                    }
424                    Err(e) => {
425                        if e.stack_trace().is_none() {
426                            let trace = ctx.capture_stack_trace();
427                            drop(guard);
428                            return Err(e.with_stack_trace(trace));
429                        }
430                        drop(guard);
431                        return Err(e);
432                    }
433                }
434            }
435        }
436        Err(e) => {
437            if e.stack_trace().is_none() {
438                let trace = ctx.capture_stack_trace();
439                drop(guard);
440                return Err(e.with_stack_trace(trace));
441            }
442            drop(guard);
443            Err(e)
444        }
445    }
446}
447
448fn eval_step(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Trampoline, SemaError> {
449    match expr.view() {
450        // Self-evaluating forms
451        ValueView::Nil
452        | ValueView::Bool(_)
453        | ValueView::Int(_)
454        | ValueView::Float(_)
455        | ValueView::String(_)
456        | ValueView::Char(_)
457        | ValueView::Thunk(_)
458        | ValueView::Bytevector(_) => Ok(Trampoline::Value(expr.clone())),
459        ValueView::Keyword(_) => Ok(Trampoline::Value(expr.clone())),
460        ValueView::Vector(items) => {
461            let mut result = Vec::with_capacity(items.len());
462            for item in items.iter() {
463                result.push(eval_value(ctx, item, env)?);
464            }
465            Ok(Trampoline::Value(Value::vector(result)))
466        }
467        ValueView::Map(map) => {
468            let mut result = std::collections::BTreeMap::new();
469            for (k, v) in map.iter() {
470                let ek = eval_value(ctx, k, env)?;
471                let ev = eval_value(ctx, v, env)?;
472                result.insert(ek, ev);
473            }
474            Ok(Trampoline::Value(Value::map(result)))
475        }
476        ValueView::HashMap(_) => Ok(Trampoline::Value(expr.clone())),
477
478        // Symbol lookup
479        ValueView::Symbol(spur) => env
480            .get(spur)
481            .map(Trampoline::Value)
482            .ok_or_else(|| SemaError::Unbound(resolve(spur))),
483
484        // Function application / special forms
485        ValueView::List(items) => {
486            if items.is_empty() {
487                return Ok(Trampoline::Value(Value::nil()));
488            }
489
490            let head = &items[0];
491            let args = &items[1..];
492
493            // O(1) special form dispatch: compare the symbol's Spur (u32 interned handle)
494            // against cached constants, avoiding string resolution entirely.
495            if let Some(spur) = head.as_symbol_spur() {
496                if let Some(result) = special_forms::try_eval_special(spur, args, env, ctx) {
497                    return result;
498                }
499            }
500
501            // Evaluate the head to get the callable
502            let func = eval_value(ctx, head, env)?;
503
504            // Look up the span of the call site expression
505            let call_span = span_of_expr(ctx, expr);
506
507            match func.view() {
508                ValueView::NativeFn(native) => {
509                    // Evaluate arguments
510                    let mut eval_args = Vec::with_capacity(args.len());
511                    for arg in args {
512                        eval_args.push(eval_value(ctx, arg, env)?);
513                    }
514                    // Push frame, call native fn
515                    let frame = CallFrame {
516                        name: native.name.to_string(),
517                        file: ctx.current_file_path(),
518                        span: call_span,
519                    };
520                    ctx.push_call_frame(frame);
521                    match (native.func)(ctx, &eval_args) {
522                        Ok(v) => {
523                            // Pop on success (native fns don't trampoline)
524                            ctx.truncate_call_stack(ctx.call_stack_depth().saturating_sub(1));
525                            Ok(Trampoline::Value(v))
526                        }
527                        // On error, leave frame for stack trace capture
528                        Err(e) => Err(e),
529                    }
530                }
531                ValueView::Lambda(lambda) => {
532                    // Evaluate arguments
533                    let mut eval_args = Vec::with_capacity(args.len());
534                    for arg in args {
535                        eval_args.push(eval_value(ctx, arg, env)?);
536                    }
537                    // Push frame — trampoline continues, eval_value guard handles cleanup
538                    let frame = CallFrame {
539                        name: lambda
540                            .name
541                            .map(resolve)
542                            .unwrap_or_else(|| "<lambda>".to_string()),
543                        file: ctx.current_file_path(),
544                        span: call_span,
545                    };
546                    ctx.push_call_frame(frame);
547                    apply_lambda(ctx, &lambda, &eval_args)
548                }
549                ValueView::Macro(mac) => {
550                    // Macros receive unevaluated arguments
551                    let expanded = apply_macro(ctx, &mac, args, env)?;
552                    // Evaluate the expansion in the current env (TCO)
553                    Ok(Trampoline::Eval(expanded, env.clone()))
554                }
555                ValueView::Keyword(spur) => {
556                    // Keywords as functions: (:key map) => (get map :key)
557                    if args.len() != 1 {
558                        let name = resolve(spur);
559                        return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
560                    }
561                    let map_val = eval_value(ctx, &args[0], env)?;
562                    let key = Value::keyword_from_spur(spur);
563                    match map_val.view() {
564                        ValueView::Map(map) => Ok(Trampoline::Value(
565                            map.get(&key).cloned().unwrap_or(Value::nil()),
566                        )),
567                        ValueView::HashMap(map) => Ok(Trampoline::Value(
568                            map.get(&key).cloned().unwrap_or(Value::nil()),
569                        )),
570                        _ => Err(SemaError::type_error("map", map_val.type_name())),
571                    }
572                }
573                _ => Err(
574                    SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
575                        .with_hint("the first element of a list must be a function or macro"),
576                ),
577            }
578        }
579
580        _other => Ok(Trampoline::Value(expr.clone())),
581    }
582}
583
584/// Apply a lambda to evaluated arguments with TCO.
585fn apply_lambda(
586    ctx: &EvalContext,
587    lambda: &Rc<Lambda>,
588    args: &[Value],
589) -> Result<Trampoline, SemaError> {
590    let new_env = Env::with_parent(Rc::new(lambda.env.clone()));
591
592    // Bind parameters
593    if let Some(rest) = lambda.rest_param {
594        if args.len() < lambda.params.len() {
595            return Err(SemaError::arity(
596                lambda
597                    .name
598                    .map(resolve)
599                    .unwrap_or_else(|| "lambda".to_string()),
600                format!("{}+", lambda.params.len()),
601                args.len(),
602            ));
603        }
604        for (param, arg) in lambda.params.iter().zip(args.iter()) {
605            new_env.set(*param, arg.clone());
606        }
607        let rest_args = args[lambda.params.len()..].to_vec();
608        new_env.set(rest, Value::list(rest_args));
609    } else {
610        if args.len() != lambda.params.len() {
611            return Err(SemaError::arity(
612                lambda
613                    .name
614                    .map(resolve)
615                    .unwrap_or_else(|| "lambda".to_string()),
616                lambda.params.len().to_string(),
617                args.len(),
618            ));
619        }
620        for (param, arg) in lambda.params.iter().zip(args.iter()) {
621            new_env.set(*param, arg.clone());
622        }
623    }
624
625    // Self-reference for recursion — just clone the Rc pointer
626    if let Some(name) = lambda.name {
627        new_env.set(name, Value::lambda_from_rc(Rc::clone(lambda)));
628    }
629
630    // Evaluate body with TCO on last expression
631    if lambda.body.is_empty() {
632        return Ok(Trampoline::Value(Value::nil()));
633    }
634    for expr in &lambda.body[..lambda.body.len() - 1] {
635        eval_value(ctx, expr, &new_env)?;
636    }
637    Ok(Trampoline::Eval(
638        lambda.body.last().unwrap().clone(),
639        new_env,
640    ))
641}
642
643/// Apply a macro: bind unevaluated args, evaluate body to produce expansion.
644pub fn apply_macro(
645    ctx: &EvalContext,
646    mac: &sema_core::Macro,
647    args: &[Value],
648    caller_env: &Env,
649) -> Result<Value, SemaError> {
650    let env = Env::with_parent(Rc::new(caller_env.clone()));
651
652    // Bind parameters to unevaluated forms
653    if let Some(rest) = mac.rest_param {
654        if args.len() < mac.params.len() {
655            return Err(SemaError::arity(
656                resolve(mac.name),
657                format!("{}+", mac.params.len()),
658                args.len(),
659            ));
660        }
661        for (param, arg) in mac.params.iter().zip(args.iter()) {
662            env.set(*param, arg.clone());
663        }
664        let rest_args = args[mac.params.len()..].to_vec();
665        env.set(rest, Value::list(rest_args));
666    } else {
667        if args.len() != mac.params.len() {
668            return Err(SemaError::arity(
669                resolve(mac.name),
670                mac.params.len().to_string(),
671                args.len(),
672            ));
673        }
674        for (param, arg) in mac.params.iter().zip(args.iter()) {
675            env.set(*param, arg.clone());
676        }
677    }
678
679    // Evaluate the macro body to get the expansion
680    let mut result = Value::nil();
681    for expr in &mac.body {
682        result = eval_value(ctx, expr, &env)?;
683    }
684    Ok(result)
685}
686
687/// Register `__vm-*` native functions that the bytecode VM calls back into
688/// the tree-walker for forms that cannot be fully compiled.
689fn register_vm_delegates(env: &Rc<Env>) {
690    // __vm-eval: evaluate an expression via the tree-walker
691    let eval_env = env.clone();
692    env.set(
693        intern("__vm-eval"),
694        Value::native_fn(NativeFn::with_ctx("__vm-eval", move |ctx, args| {
695            if args.len() != 1 {
696                return Err(SemaError::arity("eval", "1", args.len()));
697            }
698            sema_core::eval_callback(ctx, &args[0], &eval_env)
699        })),
700    );
701
702    // __vm-load: load and evaluate a file via the tree-walker
703    let load_env = env.clone();
704    env.set(
705        intern("__vm-load"),
706        Value::native_fn(NativeFn::with_ctx("__vm-load", move |ctx, args| {
707            if args.len() != 1 {
708                return Err(SemaError::arity("load", "1", args.len()));
709            }
710            ctx.sandbox.check(sema_core::Caps::FS_READ, "load")?;
711            let path = match args[0].as_str() {
712                Some(s) => s.to_string(),
713                None => return Err(SemaError::type_error("string", args[0].type_name())),
714            };
715            let full_path = if let Some(dir) = ctx.current_file_dir() {
716                dir.join(&path)
717            } else {
718                std::path::PathBuf::from(&path)
719            };
720            let content = std::fs::read_to_string(&full_path).map_err(|e| {
721                SemaError::eval(format!("load: cannot read {}: {}", full_path.display(), e))
722            })?;
723            ctx.push_file_path(full_path);
724            let result = eval_string(ctx, &content, &load_env);
725            ctx.pop_file_path();
726            result
727        })),
728    );
729
730    // __vm-import: import a module via the tree-walker
731    let import_env = env.clone();
732    env.set(
733        intern("__vm-import"),
734        Value::native_fn(NativeFn::with_ctx("__vm-import", move |ctx, args| {
735            if args.len() != 2 {
736                return Err(SemaError::arity("import", "2", args.len()));
737            }
738            ctx.sandbox.check(sema_core::Caps::FS_READ, "import")?;
739            let mut form = vec![Value::symbol("import"), args[0].clone()];
740            if let Some(items) = args[1].as_list() {
741                if !items.is_empty() {
742                    for item in items.iter() {
743                        form.push(item.clone());
744                    }
745                }
746            }
747            let import_expr = Value::list(form);
748            sema_core::eval_callback(ctx, &import_expr, &import_env)
749        })),
750    );
751
752    // __vm-defmacro: register a macro in the environment
753    let macro_env = env.clone();
754    env.set(
755        intern("__vm-defmacro"),
756        Value::native_fn(NativeFn::simple("__vm-defmacro", move |args| {
757            if args.len() != 4 {
758                return Err(SemaError::arity("defmacro", "4", args.len()));
759            }
760            let name = match args[0].as_symbol_spur() {
761                Some(s) => s,
762                None => return Err(SemaError::type_error("symbol", args[0].type_name())),
763            };
764            let params = match args[1].as_list() {
765                Some(items) => items
766                    .iter()
767                    .map(|v| match v.as_symbol_spur() {
768                        Some(s) => Ok(s),
769                        None => Err(SemaError::type_error("symbol", v.type_name())),
770                    })
771                    .collect::<Result<Vec<_>, _>>()?,
772                None => return Err(SemaError::type_error("list", args[1].type_name())),
773            };
774            let rest_param = if let Some(s) = args[2].as_symbol_spur() {
775                Some(s)
776            } else if args[2].is_nil() {
777                None
778            } else {
779                return Err(SemaError::type_error("symbol or nil", args[2].type_name()));
780            };
781            let body = vec![args[3].clone()];
782            macro_env.set(
783                name,
784                Value::macro_val(Macro {
785                    params,
786                    rest_param,
787                    body,
788                    name,
789                }),
790            );
791            Ok(Value::nil())
792        })),
793    );
794
795    // __vm-defmacro-form: delegate complete defmacro form to the tree-walker
796    let dmf_env = env.clone();
797    env.set(
798        intern("__vm-defmacro-form"),
799        Value::native_fn(NativeFn::with_ctx(
800            "__vm-defmacro-form",
801            move |ctx, args| {
802                if args.len() != 1 {
803                    return Err(SemaError::arity("defmacro-form", "1", args.len()));
804                }
805                sema_core::eval_callback(ctx, &args[0], &dmf_env)
806            },
807        )),
808    );
809
810    // __vm-define-record-type: delegate to the tree-walker
811    let drt_env = env.clone();
812    env.set(
813        intern("__vm-define-record-type"),
814        Value::native_fn(NativeFn::with_ctx(
815            "__vm-define-record-type",
816            move |ctx, args| {
817                if args.len() != 5 {
818                    return Err(SemaError::arity("define-record-type", "5", args.len()));
819                }
820                let mut ctor_form = vec![args[1].clone()];
821                if let Some(fields) = args[3].as_list() {
822                    ctor_form.extend(fields.iter().cloned());
823                }
824                let mut form = vec![
825                    Value::symbol("define-record-type"),
826                    args[0].clone(),
827                    Value::list(ctor_form),
828                    args[2].clone(),
829                ];
830                if let Some(specs) = args[4].as_list() {
831                    for spec in specs.iter() {
832                        form.push(spec.clone());
833                    }
834                }
835                sema_core::eval_callback(ctx, &Value::list(form), &drt_env)
836            },
837        )),
838    );
839
840    // __vm-delay: create a thunk with unevaluated body
841    env.set(
842        intern("__vm-delay"),
843        Value::native_fn(NativeFn::simple("__vm-delay", |args| {
844            if args.len() != 1 {
845                return Err(SemaError::arity("delay", "1", args.len()));
846            }
847            // args[0] is the unevaluated body expression (passed as a quoted constant)
848            Ok(Value::thunk(Thunk {
849                body: args[0].clone(),
850                forced: RefCell::new(None),
851            }))
852        })),
853    );
854
855    // __vm-force: force a thunk
856    let force_env = env.clone();
857    env.set(
858        intern("__vm-force"),
859        Value::native_fn(NativeFn::with_ctx("__vm-force", move |ctx, args| {
860            if args.len() != 1 {
861                return Err(SemaError::arity("force", "1", args.len()));
862            }
863            if let Some(thunk) = args[0].as_thunk_rc() {
864                if let Some(val) = thunk.forced.borrow().as_ref() {
865                    return Ok(val.clone());
866                }
867                let val = if thunk.body.as_native_fn_rc().is_some()
868                    || thunk.body.as_lambda_rc().is_some()
869                {
870                    sema_core::call_callback(ctx, &thunk.body, &[])?
871                } else {
872                    sema_core::eval_callback(ctx, &thunk.body, &force_env)?
873                };
874                *thunk.forced.borrow_mut() = Some(val.clone());
875                Ok(val)
876            } else {
877                Ok(args[0].clone())
878            }
879        })),
880    );
881
882    // __vm-macroexpand: expand a macro form via the tree-walker
883    let me_env = env.clone();
884    env.set(
885        intern("__vm-macroexpand"),
886        Value::native_fn(NativeFn::with_ctx("__vm-macroexpand", move |ctx, args| {
887            if args.len() != 1 {
888                return Err(SemaError::arity("macroexpand", "1", args.len()));
889            }
890            if let Some(items) = args[0].as_list() {
891                if !items.is_empty() {
892                    if let Some(spur) = items[0].as_symbol_spur() {
893                        if let Some(mac_val) = me_env.get(spur) {
894                            if let Some(mac) = mac_val.as_macro_rc() {
895                                return apply_macro(ctx, &mac, &items[1..], &me_env);
896                            }
897                        }
898                    }
899                }
900            }
901            Ok(args[0].clone())
902        })),
903    );
904
905    // __vm-prompt: delegate to tree-walker
906    let prompt_env = env.clone();
907    env.set(
908        intern("__vm-prompt"),
909        Value::native_fn(NativeFn::with_ctx("__vm-prompt", move |ctx, args| {
910            let mut form = vec![Value::symbol("prompt")];
911            form.extend(args.iter().cloned());
912            sema_core::eval_callback(ctx, &Value::list(form), &prompt_env)
913        })),
914    );
915
916    // __vm-message: delegate to tree-walker
917    let msg_env = env.clone();
918    env.set(
919        intern("__vm-message"),
920        Value::native_fn(NativeFn::with_ctx("__vm-message", move |ctx, args| {
921            if args.len() != 2 {
922                return Err(SemaError::arity("message", "2", args.len()));
923            }
924            let form = Value::list(vec![
925                Value::symbol("message"),
926                args[0].clone(),
927                args[1].clone(),
928            ]);
929            sema_core::eval_callback(ctx, &form, &msg_env)
930        })),
931    );
932
933    // __vm-deftool: delegate to tree-walker
934    let tool_env = env.clone();
935    env.set(
936        intern("__vm-deftool"),
937        Value::native_fn(NativeFn::with_ctx("__vm-deftool", move |ctx, args| {
938            if args.len() != 4 {
939                return Err(SemaError::arity("deftool", "4", args.len()));
940            }
941            let form = Value::list(vec![
942                Value::symbol("deftool"),
943                args[0].clone(),
944                args[1].clone(),
945                args[2].clone(),
946                args[3].clone(),
947            ]);
948            sema_core::eval_callback(ctx, &form, &tool_env)
949        })),
950    );
951
952    // __vm-defagent: delegate to tree-walker
953    let agent_env = env.clone();
954    env.set(
955        intern("__vm-defagent"),
956        Value::native_fn(NativeFn::with_ctx("__vm-defagent", move |ctx, args| {
957            if args.len() != 2 {
958                return Err(SemaError::arity("defagent", "2", args.len()));
959            }
960            let form = Value::list(vec![
961                Value::symbol("defagent"),
962                args[0].clone(),
963                args[1].clone(),
964            ]);
965            sema_core::eval_callback(ctx, &form, &agent_env)
966        })),
967    );
968}