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            let err = SemaError::Unbound(resolve(spur));
269            let trace = ctx.capture_stack_trace();
270            return Err(err.with_stack_trace(trace));
271        }
272        _ => {}
273    }
274
275    let depth = ctx.eval_depth.get();
276    ctx.eval_depth.set(depth + 1);
277    if depth == 0 {
278        ctx.eval_steps.set(0);
279    }
280    if depth > MAX_EVAL_DEPTH {
281        ctx.eval_depth.set(ctx.eval_depth.get().saturating_sub(1));
282        return Err(SemaError::eval(format!(
283            "maximum eval depth exceeded ({MAX_EVAL_DEPTH})"
284        )));
285    }
286
287    let result = eval_value_inner(ctx, expr, env);
288
289    ctx.eval_depth.set(ctx.eval_depth.get().saturating_sub(1));
290    result
291}
292
293/// Call a function value with already-evaluated arguments.
294/// This is the public API for stdlib functions that need to invoke callbacks.
295pub fn call_value(ctx: &EvalContext, func: &Value, args: &[Value]) -> EvalResult {
296    match func.view() {
297        ValueView::NativeFn(native) => (native.func)(ctx, args),
298        ValueView::Lambda(lambda) => {
299            let new_env = Env::with_parent(Rc::new(lambda.env.clone()));
300
301            if let Some(rest) = lambda.rest_param {
302                if args.len() < lambda.params.len() {
303                    return Err(SemaError::arity(
304                        lambda
305                            .name
306                            .map(resolve)
307                            .unwrap_or_else(|| "lambda".to_string()),
308                        format!("{}+", lambda.params.len()),
309                        args.len(),
310                    ));
311                }
312                for (param, arg) in lambda.params.iter().zip(args.iter()) {
313                    new_env.set(*param, arg.clone());
314                }
315                let rest_args = args[lambda.params.len()..].to_vec();
316                new_env.set(rest, Value::list(rest_args));
317            } else {
318                if args.len() != lambda.params.len() {
319                    return Err(SemaError::arity(
320                        lambda
321                            .name
322                            .map(resolve)
323                            .unwrap_or_else(|| "lambda".to_string()),
324                        lambda.params.len().to_string(),
325                        args.len(),
326                    ));
327                }
328                for (param, arg) in lambda.params.iter().zip(args.iter()) {
329                    new_env.set(*param, arg.clone());
330                }
331            }
332
333            if let Some(name) = lambda.name {
334                new_env.set(name, Value::lambda_from_rc(Rc::clone(&lambda)));
335            }
336
337            let mut result = Value::nil();
338            for expr in &lambda.body {
339                result = eval_value(ctx, expr, &new_env)?;
340            }
341            Ok(result)
342        }
343        ValueView::Keyword(spur) => {
344            if args.len() != 1 {
345                let name = resolve(spur);
346                return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
347            }
348            let key = Value::keyword_from_spur(spur);
349            match args[0].view() {
350                ValueView::Map(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
351                ValueView::HashMap(map) => Ok(map.get(&key).cloned().unwrap_or(Value::nil())),
352                _ => Err(SemaError::type_error("map", args[0].type_name())),
353            }
354        }
355        _ => Err(
356            SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
357                .with_hint("expected a function, lambda, or keyword"),
358        ),
359    }
360}
361
362fn eval_value_inner(ctx: &EvalContext, expr: &Value, env: &Env) -> EvalResult {
363    let entry_depth = ctx.call_stack_depth();
364    let guard = CallStackGuard { ctx, entry_depth };
365    let limit = ctx.eval_step_limit.get();
366
367    // First iteration: use borrowed expr/env to avoid cloning
368    if limit > 0 {
369        let v = ctx.eval_steps.get() + 1;
370        ctx.eval_steps.set(v);
371        if v > limit {
372            return Err(SemaError::eval("eval step limit exceeded".to_string()));
373        }
374    }
375
376    match eval_step(ctx, expr, env) {
377        Ok(Trampoline::Value(v)) => {
378            drop(guard);
379            Ok(v)
380        }
381        Ok(Trampoline::Eval(next_expr, next_env)) => {
382            // Need to continue — enter the trampoline loop
383            let mut current_expr = next_expr;
384            let mut current_env = next_env;
385
386            // Trim call stack for TCO
387            {
388                let mut stack = ctx.call_stack.borrow_mut();
389                if stack.len() > entry_depth + 1 {
390                    let top = stack.last().cloned();
391                    stack.truncate(entry_depth);
392                    if let Some(frame) = top {
393                        stack.push(frame);
394                    }
395                }
396            }
397
398            loop {
399                if limit > 0 {
400                    let v = ctx.eval_steps.get() + 1;
401                    ctx.eval_steps.set(v);
402                    if v > limit {
403                        return Err(SemaError::eval("eval step limit exceeded".to_string()));
404                    }
405                }
406
407                match eval_step(ctx, &current_expr, &current_env) {
408                    Ok(Trampoline::Value(v)) => {
409                        drop(guard);
410                        return Ok(v);
411                    }
412                    Ok(Trampoline::Eval(next_expr, next_env)) => {
413                        {
414                            let mut stack = ctx.call_stack.borrow_mut();
415                            if stack.len() > entry_depth + 1 {
416                                let top = stack.last().cloned();
417                                stack.truncate(entry_depth);
418                                if let Some(frame) = top {
419                                    stack.push(frame);
420                                }
421                            }
422                        }
423                        current_expr = next_expr;
424                        current_env = next_env;
425                    }
426                    Err(e) => {
427                        if e.stack_trace().is_none() {
428                            let trace = ctx.capture_stack_trace();
429                            drop(guard);
430                            return Err(e.with_stack_trace(trace));
431                        }
432                        drop(guard);
433                        return Err(e);
434                    }
435                }
436            }
437        }
438        Err(e) => {
439            if e.stack_trace().is_none() {
440                let trace = ctx.capture_stack_trace();
441                drop(guard);
442                return Err(e.with_stack_trace(trace));
443            }
444            drop(guard);
445            Err(e)
446        }
447    }
448}
449
450fn eval_step(ctx: &EvalContext, expr: &Value, env: &Env) -> Result<Trampoline, SemaError> {
451    match expr.view() {
452        // Self-evaluating forms
453        ValueView::Nil
454        | ValueView::Bool(_)
455        | ValueView::Int(_)
456        | ValueView::Float(_)
457        | ValueView::String(_)
458        | ValueView::Char(_)
459        | ValueView::Thunk(_)
460        | ValueView::Bytevector(_) => Ok(Trampoline::Value(expr.clone())),
461        ValueView::Keyword(_) => Ok(Trampoline::Value(expr.clone())),
462        ValueView::Vector(items) => {
463            let mut result = Vec::with_capacity(items.len());
464            for item in items.iter() {
465                result.push(eval_value(ctx, item, env)?);
466            }
467            Ok(Trampoline::Value(Value::vector(result)))
468        }
469        ValueView::Map(map) => {
470            let mut result = std::collections::BTreeMap::new();
471            for (k, v) in map.iter() {
472                let ek = eval_value(ctx, k, env)?;
473                let ev = eval_value(ctx, v, env)?;
474                result.insert(ek, ev);
475            }
476            Ok(Trampoline::Value(Value::map(result)))
477        }
478        ValueView::HashMap(_) => Ok(Trampoline::Value(expr.clone())),
479
480        // Symbol lookup
481        ValueView::Symbol(spur) => env
482            .get(spur)
483            .map(Trampoline::Value)
484            .ok_or_else(|| SemaError::Unbound(resolve(spur))),
485
486        // Function application / special forms
487        ValueView::List(items) => {
488            if items.is_empty() {
489                return Ok(Trampoline::Value(Value::nil()));
490            }
491
492            let head = &items[0];
493            let args = &items[1..];
494
495            // O(1) special form dispatch: compare the symbol's Spur (u32 interned handle)
496            // against cached constants, avoiding string resolution entirely.
497            if let Some(spur) = head.as_symbol_spur() {
498                if let Some(result) = special_forms::try_eval_special(spur, args, env, ctx) {
499                    return result;
500                }
501            }
502
503            // Evaluate the head to get the callable
504            let func = eval_value(ctx, head, env)?;
505
506            // Look up the span of the call site expression
507            let call_span = span_of_expr(ctx, expr);
508
509            match func.view() {
510                ValueView::NativeFn(native) => {
511                    // Evaluate arguments
512                    let mut eval_args = Vec::with_capacity(args.len());
513                    for arg in args {
514                        eval_args.push(eval_value(ctx, arg, env)?);
515                    }
516                    // Push frame, call native fn
517                    let frame = CallFrame {
518                        name: native.name.to_string(),
519                        file: ctx.current_file_path(),
520                        span: call_span,
521                    };
522                    ctx.push_call_frame(frame);
523                    match (native.func)(ctx, &eval_args) {
524                        Ok(v) => {
525                            // Pop on success (native fns don't trampoline)
526                            ctx.truncate_call_stack(ctx.call_stack_depth().saturating_sub(1));
527                            Ok(Trampoline::Value(v))
528                        }
529                        // On error, leave frame for stack trace capture
530                        Err(e) => Err(e),
531                    }
532                }
533                ValueView::Lambda(lambda) => {
534                    // Evaluate arguments
535                    let mut eval_args = Vec::with_capacity(args.len());
536                    for arg in args {
537                        eval_args.push(eval_value(ctx, arg, env)?);
538                    }
539                    // Push frame — trampoline continues, eval_value guard handles cleanup
540                    let frame = CallFrame {
541                        name: lambda
542                            .name
543                            .map(resolve)
544                            .unwrap_or_else(|| "<lambda>".to_string()),
545                        file: ctx.current_file_path(),
546                        span: call_span,
547                    };
548                    ctx.push_call_frame(frame);
549                    apply_lambda(ctx, &lambda, &eval_args)
550                }
551                ValueView::Macro(mac) => {
552                    // Macros receive unevaluated arguments
553                    let expanded = apply_macro(ctx, &mac, args, env)?;
554                    // Evaluate the expansion in the current env (TCO)
555                    Ok(Trampoline::Eval(expanded, env.clone()))
556                }
557                ValueView::Keyword(spur) => {
558                    // Keywords as functions: (:key map) => (get map :key)
559                    if args.len() != 1 {
560                        let name = resolve(spur);
561                        return Err(SemaError::arity(format!(":{name}"), "1", args.len()));
562                    }
563                    let map_val = eval_value(ctx, &args[0], env)?;
564                    let key = Value::keyword_from_spur(spur);
565                    match map_val.view() {
566                        ValueView::Map(map) => Ok(Trampoline::Value(
567                            map.get(&key).cloned().unwrap_or(Value::nil()),
568                        )),
569                        ValueView::HashMap(map) => Ok(Trampoline::Value(
570                            map.get(&key).cloned().unwrap_or(Value::nil()),
571                        )),
572                        _ => Err(SemaError::type_error("map", map_val.type_name())),
573                    }
574                }
575                _ => Err(
576                    SemaError::eval(format!("not callable: {} ({})", func, func.type_name()))
577                        .with_hint("the first element of a list must be a function or macro"),
578                ),
579            }
580        }
581
582        _other => Ok(Trampoline::Value(expr.clone())),
583    }
584}
585
586/// Apply a lambda to evaluated arguments with TCO.
587fn apply_lambda(
588    ctx: &EvalContext,
589    lambda: &Rc<Lambda>,
590    args: &[Value],
591) -> Result<Trampoline, SemaError> {
592    let new_env = Env::with_parent(Rc::new(lambda.env.clone()));
593
594    // Bind parameters
595    if let Some(rest) = lambda.rest_param {
596        if args.len() < lambda.params.len() {
597            return Err(SemaError::arity(
598                lambda
599                    .name
600                    .map(resolve)
601                    .unwrap_or_else(|| "lambda".to_string()),
602                format!("{}+", lambda.params.len()),
603                args.len(),
604            ));
605        }
606        for (param, arg) in lambda.params.iter().zip(args.iter()) {
607            new_env.set(*param, arg.clone());
608        }
609        let rest_args = args[lambda.params.len()..].to_vec();
610        new_env.set(rest, Value::list(rest_args));
611    } else {
612        if args.len() != lambda.params.len() {
613            return Err(SemaError::arity(
614                lambda
615                    .name
616                    .map(resolve)
617                    .unwrap_or_else(|| "lambda".to_string()),
618                lambda.params.len().to_string(),
619                args.len(),
620            ));
621        }
622        for (param, arg) in lambda.params.iter().zip(args.iter()) {
623            new_env.set(*param, arg.clone());
624        }
625    }
626
627    // Self-reference for recursion — just clone the Rc pointer
628    if let Some(name) = lambda.name {
629        new_env.set(name, Value::lambda_from_rc(Rc::clone(lambda)));
630    }
631
632    // Evaluate body with TCO on last expression
633    if lambda.body.is_empty() {
634        return Ok(Trampoline::Value(Value::nil()));
635    }
636    for expr in &lambda.body[..lambda.body.len() - 1] {
637        eval_value(ctx, expr, &new_env)?;
638    }
639    Ok(Trampoline::Eval(
640        lambda.body.last().unwrap().clone(),
641        new_env,
642    ))
643}
644
645/// Apply a macro: bind unevaluated args, evaluate body to produce expansion.
646pub fn apply_macro(
647    ctx: &EvalContext,
648    mac: &sema_core::Macro,
649    args: &[Value],
650    caller_env: &Env,
651) -> Result<Value, SemaError> {
652    let env = Env::with_parent(Rc::new(caller_env.clone()));
653
654    // Bind parameters to unevaluated forms
655    if let Some(rest) = mac.rest_param {
656        if args.len() < mac.params.len() {
657            return Err(SemaError::arity(
658                resolve(mac.name),
659                format!("{}+", mac.params.len()),
660                args.len(),
661            ));
662        }
663        for (param, arg) in mac.params.iter().zip(args.iter()) {
664            env.set(*param, arg.clone());
665        }
666        let rest_args = args[mac.params.len()..].to_vec();
667        env.set(rest, Value::list(rest_args));
668    } else {
669        if args.len() != mac.params.len() {
670            return Err(SemaError::arity(
671                resolve(mac.name),
672                mac.params.len().to_string(),
673                args.len(),
674            ));
675        }
676        for (param, arg) in mac.params.iter().zip(args.iter()) {
677            env.set(*param, arg.clone());
678        }
679    }
680
681    // Evaluate the macro body to get the expansion
682    let mut result = Value::nil();
683    for expr in &mac.body {
684        result = eval_value(ctx, expr, &env)?;
685    }
686    Ok(result)
687}
688
689/// Register `__vm-*` native functions that the bytecode VM calls back into
690/// the tree-walker for forms that cannot be fully compiled.
691fn register_vm_delegates(env: &Rc<Env>) {
692    // __vm-eval: evaluate an expression via the tree-walker
693    let eval_env = env.clone();
694    env.set(
695        intern("__vm-eval"),
696        Value::native_fn(NativeFn::with_ctx("__vm-eval", move |ctx, args| {
697            if args.len() != 1 {
698                return Err(SemaError::arity("eval", "1", args.len()));
699            }
700            sema_core::eval_callback(ctx, &args[0], &eval_env)
701        })),
702    );
703
704    // __vm-load: load and evaluate a file via the tree-walker
705    let load_env = env.clone();
706    env.set(
707        intern("__vm-load"),
708        Value::native_fn(NativeFn::with_ctx("__vm-load", move |ctx, args| {
709            if args.len() != 1 {
710                return Err(SemaError::arity("load", "1", args.len()));
711            }
712            ctx.sandbox.check(sema_core::Caps::FS_READ, "load")?;
713            let path = match args[0].as_str() {
714                Some(s) => s.to_string(),
715                None => return Err(SemaError::type_error("string", args[0].type_name())),
716            };
717            let full_path = if let Some(dir) = ctx.current_file_dir() {
718                dir.join(&path)
719            } else {
720                std::path::PathBuf::from(&path)
721            };
722            let content = std::fs::read_to_string(&full_path).map_err(|e| {
723                SemaError::eval(format!("load: cannot read {}: {}", full_path.display(), e))
724            })?;
725            ctx.push_file_path(full_path);
726            let result = eval_string(ctx, &content, &load_env);
727            ctx.pop_file_path();
728            result
729        })),
730    );
731
732    // __vm-import: import a module via the tree-walker
733    let import_env = env.clone();
734    env.set(
735        intern("__vm-import"),
736        Value::native_fn(NativeFn::with_ctx("__vm-import", move |ctx, args| {
737            if args.len() != 2 {
738                return Err(SemaError::arity("import", "2", args.len()));
739            }
740            ctx.sandbox.check(sema_core::Caps::FS_READ, "import")?;
741            let mut form = vec![Value::symbol("import"), args[0].clone()];
742            if let Some(items) = args[1].as_list() {
743                if !items.is_empty() {
744                    for item in items.iter() {
745                        form.push(item.clone());
746                    }
747                }
748            }
749            let import_expr = Value::list(form);
750            sema_core::eval_callback(ctx, &import_expr, &import_env)
751        })),
752    );
753
754    // __vm-defmacro: register a macro in the environment
755    let macro_env = env.clone();
756    env.set(
757        intern("__vm-defmacro"),
758        Value::native_fn(NativeFn::simple("__vm-defmacro", move |args| {
759            if args.len() != 4 {
760                return Err(SemaError::arity("defmacro", "4", args.len()));
761            }
762            let name = match args[0].as_symbol_spur() {
763                Some(s) => s,
764                None => return Err(SemaError::type_error("symbol", args[0].type_name())),
765            };
766            let params = match args[1].as_list() {
767                Some(items) => items
768                    .iter()
769                    .map(|v| match v.as_symbol_spur() {
770                        Some(s) => Ok(s),
771                        None => Err(SemaError::type_error("symbol", v.type_name())),
772                    })
773                    .collect::<Result<Vec<_>, _>>()?,
774                None => return Err(SemaError::type_error("list", args[1].type_name())),
775            };
776            let rest_param = if let Some(s) = args[2].as_symbol_spur() {
777                Some(s)
778            } else if args[2].is_nil() {
779                None
780            } else {
781                return Err(SemaError::type_error("symbol or nil", args[2].type_name()));
782            };
783            let body = vec![args[3].clone()];
784            macro_env.set(
785                name,
786                Value::macro_val(Macro {
787                    params,
788                    rest_param,
789                    body,
790                    name,
791                }),
792            );
793            Ok(Value::nil())
794        })),
795    );
796
797    // __vm-defmacro-form: delegate complete defmacro form to the tree-walker
798    let dmf_env = env.clone();
799    env.set(
800        intern("__vm-defmacro-form"),
801        Value::native_fn(NativeFn::with_ctx(
802            "__vm-defmacro-form",
803            move |ctx, args| {
804                if args.len() != 1 {
805                    return Err(SemaError::arity("defmacro-form", "1", args.len()));
806                }
807                sema_core::eval_callback(ctx, &args[0], &dmf_env)
808            },
809        )),
810    );
811
812    // __vm-define-record-type: delegate to the tree-walker
813    let drt_env = env.clone();
814    env.set(
815        intern("__vm-define-record-type"),
816        Value::native_fn(NativeFn::with_ctx(
817            "__vm-define-record-type",
818            move |ctx, args| {
819                if args.len() != 5 {
820                    return Err(SemaError::arity("define-record-type", "5", args.len()));
821                }
822                let mut ctor_form = vec![args[1].clone()];
823                if let Some(fields) = args[3].as_list() {
824                    ctor_form.extend(fields.iter().cloned());
825                }
826                let mut form = vec![
827                    Value::symbol("define-record-type"),
828                    args[0].clone(),
829                    Value::list(ctor_form),
830                    args[2].clone(),
831                ];
832                if let Some(specs) = args[4].as_list() {
833                    for spec in specs.iter() {
834                        form.push(spec.clone());
835                    }
836                }
837                sema_core::eval_callback(ctx, &Value::list(form), &drt_env)
838            },
839        )),
840    );
841
842    // __vm-delay: create a thunk with unevaluated body
843    env.set(
844        intern("__vm-delay"),
845        Value::native_fn(NativeFn::simple("__vm-delay", |args| {
846            if args.len() != 1 {
847                return Err(SemaError::arity("delay", "1", args.len()));
848            }
849            // args[0] is the unevaluated body expression (passed as a quoted constant)
850            Ok(Value::thunk(Thunk {
851                body: args[0].clone(),
852                forced: RefCell::new(None),
853            }))
854        })),
855    );
856
857    // __vm-force: force a thunk
858    let force_env = env.clone();
859    env.set(
860        intern("__vm-force"),
861        Value::native_fn(NativeFn::with_ctx("__vm-force", move |ctx, args| {
862            if args.len() != 1 {
863                return Err(SemaError::arity("force", "1", args.len()));
864            }
865            if let Some(thunk) = args[0].as_thunk_rc() {
866                if let Some(val) = thunk.forced.borrow().as_ref() {
867                    return Ok(val.clone());
868                }
869                let val = if thunk.body.as_native_fn_rc().is_some()
870                    || thunk.body.as_lambda_rc().is_some()
871                {
872                    sema_core::call_callback(ctx, &thunk.body, &[])?
873                } else {
874                    sema_core::eval_callback(ctx, &thunk.body, &force_env)?
875                };
876                *thunk.forced.borrow_mut() = Some(val.clone());
877                Ok(val)
878            } else {
879                Ok(args[0].clone())
880            }
881        })),
882    );
883
884    // __vm-macroexpand: expand a macro form via the tree-walker
885    let me_env = env.clone();
886    env.set(
887        intern("__vm-macroexpand"),
888        Value::native_fn(NativeFn::with_ctx("__vm-macroexpand", move |ctx, args| {
889            if args.len() != 1 {
890                return Err(SemaError::arity("macroexpand", "1", args.len()));
891            }
892            if let Some(items) = args[0].as_list() {
893                if !items.is_empty() {
894                    if let Some(spur) = items[0].as_symbol_spur() {
895                        if let Some(mac_val) = me_env.get(spur) {
896                            if let Some(mac) = mac_val.as_macro_rc() {
897                                return apply_macro(ctx, &mac, &items[1..], &me_env);
898                            }
899                        }
900                    }
901                }
902            }
903            Ok(args[0].clone())
904        })),
905    );
906
907    // __vm-prompt: delegate to tree-walker
908    let prompt_env = env.clone();
909    env.set(
910        intern("__vm-prompt"),
911        Value::native_fn(NativeFn::with_ctx("__vm-prompt", move |ctx, args| {
912            let mut form = vec![Value::symbol("prompt")];
913            form.extend(args.iter().cloned());
914            sema_core::eval_callback(ctx, &Value::list(form), &prompt_env)
915        })),
916    );
917
918    // __vm-message: delegate to tree-walker
919    let msg_env = env.clone();
920    env.set(
921        intern("__vm-message"),
922        Value::native_fn(NativeFn::with_ctx("__vm-message", move |ctx, args| {
923            if args.len() != 2 {
924                return Err(SemaError::arity("message", "2", args.len()));
925            }
926            let form = Value::list(vec![
927                Value::symbol("message"),
928                args[0].clone(),
929                args[1].clone(),
930            ]);
931            sema_core::eval_callback(ctx, &form, &msg_env)
932        })),
933    );
934
935    // __vm-deftool: delegate to tree-walker
936    let tool_env = env.clone();
937    env.set(
938        intern("__vm-deftool"),
939        Value::native_fn(NativeFn::with_ctx("__vm-deftool", move |ctx, args| {
940            if args.len() != 4 {
941                return Err(SemaError::arity("deftool", "4", args.len()));
942            }
943            let form = Value::list(vec![
944                Value::symbol("deftool"),
945                args[0].clone(),
946                args[1].clone(),
947                args[2].clone(),
948                args[3].clone(),
949            ]);
950            sema_core::eval_callback(ctx, &form, &tool_env)
951        })),
952    );
953
954    // __vm-defagent: delegate to tree-walker
955    let agent_env = env.clone();
956    env.set(
957        intern("__vm-defagent"),
958        Value::native_fn(NativeFn::with_ctx("__vm-defagent", move |ctx, args| {
959            if args.len() != 2 {
960                return Err(SemaError::arity("defagent", "2", args.len()));
961            }
962            let form = Value::list(vec![
963                Value::symbol("defagent"),
964                args[0].clone(),
965                args[1].clone(),
966            ]);
967            sema_core::eval_callback(ctx, &form, &agent_env)
968        })),
969    );
970}