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