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