Skip to main content

tatara_lisp_eval/vm/
run.rs

1//! VM run loop — interprets a `Chunk` against the host's
2//! `Interpreter<H>` (for native-fn dispatch + global env access).
3//!
4//! The VM is a thin wrapper: stack of `Value`s + stack of `Frame`s +
5//! IP. Native fns and closures both go through `Interpreter::apply`
6//! semantics — same `FnRegistry`, same `Env`, same Value type as the
7//! tree-walker. This means primitives written for the eval crate
8//! (arithmetic, list, hash-map, channel, ...) just work.
9
10use std::sync::{Arc, Mutex};
11
12use tatara_lisp::Span;
13use thiserror::Error;
14
15use super::chunk::{CaptureSource, Chunk, CompiledFn};
16use super::op::Op;
17use crate::eval::Interpreter;
18use crate::value::Value;
19
20#[derive(Debug, Error)]
21pub enum VmError {
22    #[error("stack underflow at op {ip}")]
23    Underflow { ip: usize },
24    #[error("unbound symbol `{name}` at {at}")]
25    Unbound { name: String, at: Span },
26    #[error("not callable: {kind} at {at}")]
27    NotCallable { kind: &'static str, at: Span },
28    #[error("arity mismatch: expected {expected}, got {got} at {at}")]
29    Arity {
30        expected: usize,
31        got: usize,
32        at: Span,
33    },
34    #[error("eval error: {0}")]
35    Eval(#[from] crate::error::EvalError),
36    #[error("local index out of bounds: {0}")]
37    BadLocal(usize),
38}
39
40/// One installed exception handler. Pushed by `PushHandler`, popped
41/// by `PopHandler` or activated when an error unwinds through this
42/// frame.
43#[derive(Debug, Clone, Copy)]
44struct HandlerRecord {
45    /// IP to jump to on error.
46    catch_ip: usize,
47    /// Local slot to store the error Value into before resuming.
48    error_local: usize,
49    /// Stack length AT push time — on error, the runtime truncates
50    /// any stale temporaries left over from a partially-evaluated
51    /// body so handler logic starts on a clean slate.
52    stack_at_push: usize,
53}
54
55/// One activation record. The VM is a stack of these.
56struct Frame {
57    /// The function being executed.
58    func: Arc<CompiledFn>,
59    /// IP within `func.ops`.
60    ip: usize,
61    /// First local-slot index in the shared value stack — locals
62    /// live at `stack[locals_base..locals_base + func.locals]`.
63    locals_base: usize,
64    /// First slot ABOVE the locals — temporaries push here.
65    /// (Equal to `locals_base + func.locals` at frame entry, never moves.)
66    stack_base: usize,
67    /// Captured upvalues from this closure's enclosing scope. Indexed
68    /// by `LoadCaptured(idx)` / `StoreCaptured(idx)`. Each cell is
69    /// shared via `Mutex` so `set!` on a captured name is visible to
70    /// other closures that captured the same outer slot.
71    captures: Vec<Arc<Mutex<Value>>>,
72    /// Heap-promoted cells for THIS frame's locals that have been
73    /// captured by inner closures. Keyed by local slot index;
74    /// established lazily on first `MakeClosure` that references the
75    /// slot. After promotion, `set!`/`StoreLocal` writes through the
76    /// cell so every closure capturing the slot sees the change.
77    local_cells: std::collections::HashMap<usize, Arc<Mutex<Value>>>,
78    /// Active error handlers on this frame, innermost last.
79    handlers: Vec<HandlerRecord>,
80}
81
82/// The VM. Owns the stack + frame stack while a program runs.
83pub struct Vm {
84    stack: Vec<Value>,
85    frames: Vec<Frame>,
86}
87
88impl Vm {
89    pub fn new() -> Self {
90        Self {
91            stack: Vec::with_capacity(256),
92            frames: Vec::with_capacity(64),
93        }
94    }
95
96    /// Execute a chunk against the host interpreter. Returns the
97    /// final value (the program's result). Errors that escape an
98    /// active `(try ...)` handler propagate to the caller.
99    ///
100    /// Convenience wrapper for callers holding a `&Chunk` — clones into
101    /// an `Arc<Chunk>`. Prefer `run_arc` when the chunk is already
102    /// `Arc`-shared (e.g. from the host interpreter's compile cache).
103    pub fn run<H: 'static>(
104        &mut self,
105        chunk: &Chunk,
106        interp: &mut Interpreter<H>,
107        host: &mut H,
108    ) -> Result<Value, VmError> {
109        let chunk_arc = Arc::new(chunk.clone());
110        self.run_arc(chunk_arc, interp, host)
111    }
112
113    /// Like `run`, but takes ownership of an `Arc<Chunk>` so closures
114    /// produced via `MakeClosure` can carry a cheap clone of the chunk
115    /// for self-contained re-invocation through `Caller::apply_value`.
116    pub fn run_arc<H: 'static>(
117        &mut self,
118        chunk: Arc<Chunk>,
119        interp: &mut Interpreter<H>,
120        host: &mut H,
121    ) -> Result<Value, VmError> {
122        // Reset state.
123        self.stack.clear();
124        self.frames.clear();
125        // Allocate the top-level frame.
126        let top_func = Arc::new(chunk.top.clone());
127        let top_locals = top_func.locals;
128        let top_frame = Frame {
129            func: top_func.clone(),
130            ip: 0,
131            locals_base: 0,
132            stack_base: top_locals,
133            captures: Vec::new(),
134            local_cells: std::collections::HashMap::new(),
135            handlers: Vec::new(),
136        };
137        // Reserve slots for locals.
138        for _ in 0..top_locals {
139            self.stack.push(Value::Nil);
140        }
141        self.frames.push(top_frame);
142
143        // Drive the run loop with handler-aware error routing.
144        self.run_with_handlers(&chunk, interp, host)
145    }
146
147    /// Inner main interpret loop — runs until Halt, Return at top,
148    /// or an error. Errors are caught by `run_with_handlers` which
149    /// routes them through any installed `(try ...)` handlers.
150    /// `chunk` is `&Arc<Chunk>` (not `&Chunk`) so `Op::MakeClosure` can
151    /// stash a cheap `Arc::clone` into the produced `CompiledClosure`.
152    fn run_inner<H: 'static>(
153        &mut self,
154        chunk: &Arc<Chunk>,
155        interp: &mut Interpreter<H>,
156        host: &mut H,
157    ) -> Result<Value, VmError> {
158        loop {
159            // Snapshot the current frame fields to avoid simultaneous
160            // borrows. We index by `frames.last()` cheaply.
161            let frame_idx = self.frames.len() - 1;
162            let (op, span);
163            {
164                let f = &self.frames[frame_idx];
165                if f.ip >= f.func.ops.len() {
166                    // Implicit Halt — should never happen with a
167                    // well-compiled chunk; defensive abort.
168                    return Ok(self.pop_or_nil());
169                }
170                op = f.func.ops[f.ip].clone();
171                span = f.func.spans.get(f.ip).copied().unwrap_or(Span::synthetic());
172            }
173            self.frames[frame_idx].ip += 1;
174
175            match op {
176                Op::Halt => {
177                    return Ok(self.pop_or_nil());
178                }
179                Op::Nil => self.stack.push(Value::Nil),
180                Op::True => self.stack.push(Value::Bool(true)),
181                Op::False => self.stack.push(Value::Bool(false)),
182                Op::Int(n) => self.stack.push(Value::Int(n)),
183                Op::Const(idx) => {
184                    let v = chunk.consts.get(idx).clone();
185                    self.stack.push(v);
186                }
187
188                Op::Pop => {
189                    self.stack.pop().ok_or(VmError::Underflow { ip: 0 })?;
190                }
191                Op::Dup => {
192                    let v = self
193                        .stack
194                        .last()
195                        .ok_or(VmError::Underflow { ip: 0 })?
196                        .clone();
197                    self.stack.push(v);
198                }
199
200                Op::LoadLocal(idx) => {
201                    let f = &self.frames[frame_idx];
202                    // If the slot has been promoted to a cell (because
203                    // an inner closure captured it), read through the
204                    // cell so we see any set! made via StoreCaptured.
205                    if let Some(cell) = f.local_cells.get(&idx) {
206                        let v = cell.lock().unwrap().clone();
207                        self.stack.push(v);
208                    } else {
209                        let abs = f.locals_base + idx;
210                        let v = self
211                            .stack
212                            .get(abs)
213                            .cloned()
214                            .ok_or(VmError::BadLocal(idx))?;
215                        self.stack.push(v);
216                    }
217                }
218                Op::StoreLocal(idx) => {
219                    let v = self.stack.pop().ok_or(VmError::Underflow { ip: 0 })?;
220                    let f = &self.frames[frame_idx];
221                    // Same dual path: write through the cell when
222                    // the slot has been promoted.
223                    if let Some(cell) = f.local_cells.get(&idx).cloned() {
224                        *cell.lock().unwrap() = v;
225                    } else {
226                        let abs = f.locals_base + idx;
227                        if abs >= self.stack.len() {
228                            return Err(VmError::BadLocal(idx));
229                        }
230                        self.stack[abs] = v;
231                    }
232                }
233
234                Op::LoadCaptured(idx) => {
235                    let f = &self.frames[frame_idx];
236                    let cell = f
237                        .captures
238                        .get(idx)
239                        .cloned()
240                        .ok_or(VmError::BadLocal(idx))?;
241                    let v = cell.lock().unwrap().clone();
242                    self.stack.push(v);
243                }
244                Op::StoreCaptured(idx) => {
245                    let v = self.stack.pop().ok_or(VmError::Underflow { ip: 0 })?;
246                    let f = &self.frames[frame_idx];
247                    let cell = f
248                        .captures
249                        .get(idx)
250                        .cloned()
251                        .ok_or(VmError::BadLocal(idx))?;
252                    *cell.lock().unwrap() = v;
253                }
254
255                Op::LoadGlobal(name_idx) => {
256                    let name = chunk.names.get(name_idx).clone();
257                    let v = self
258                        .lookup_global(interp, &name)
259                        .ok_or_else(|| VmError::Unbound {
260                            name: name.to_string(),
261                            at: span,
262                        })?;
263                    self.stack.push(v);
264                }
265                Op::StoreGlobal(name_idx) => {
266                    let name = chunk.names.get(name_idx).clone();
267                    let v = self.stack.pop().ok_or(VmError::Underflow { ip: 0 })?;
268                    interp.define_global(name, v);
269                }
270
271                Op::Jmp(target) => {
272                    self.frames[frame_idx].ip = target;
273                }
274                Op::JmpNot(target) => {
275                    let v = self.stack.pop().ok_or(VmError::Underflow { ip: 0 })?;
276                    if !v.is_truthy() {
277                        self.frames[frame_idx].ip = target;
278                    }
279                }
280                Op::JmpIf(target) => {
281                    let v = self.stack.pop().ok_or(VmError::Underflow { ip: 0 })?;
282                    if v.is_truthy() {
283                        self.frames[frame_idx].ip = target;
284                    }
285                }
286
287                Op::MakeClosure(fn_idx) => {
288                    let body = chunk.fn_table[fn_idx].clone();
289                    // Build the captures array for the new closure.
290                    // For each capture descriptor: pull the cell
291                    // from the appropriate slot in the CURRENT frame.
292                    // Promotion: when a Local source first appears,
293                    // promote the slot to a heap cell (insert into
294                    // local_cells, copy the current stack value into
295                    // the cell). Subsequent MakeClosures referencing
296                    // the same slot reuse that same cell — so set!
297                    // through any closure is observable to all
298                    // closures sharing the slot.
299                    let mut closure_captures: Vec<Arc<Mutex<Value>>> =
300                        Vec::with_capacity(body.captures.len());
301                    for (_, source) in &body.captures {
302                        let cell = match source {
303                            CaptureSource::Local(local_idx) => {
304                                let f = &mut self.frames[frame_idx];
305                                if let Some(existing) = f.local_cells.get(local_idx).cloned() {
306                                    existing
307                                } else {
308                                    let abs = f.locals_base + local_idx;
309                                    let v = self
310                                        .stack
311                                        .get(abs)
312                                        .cloned()
313                                        .ok_or(VmError::BadLocal(*local_idx))?;
314                                    let cell = Arc::new(Mutex::new(v));
315                                    self.frames[frame_idx]
316                                        .local_cells
317                                        .insert(*local_idx, cell.clone());
318                                    cell
319                                }
320                            }
321                            CaptureSource::Captured(cap_idx) => self.frames[frame_idx]
322                                .captures
323                                .get(*cap_idx)
324                                .cloned()
325                                .ok_or(VmError::BadLocal(*cap_idx))?,
326                        };
327                        closure_captures.push(cell);
328                    }
329                    let compiled = CompiledClosure {
330                        body: Arc::new(body),
331                        captures: closure_captures,
332                        chunk: Arc::clone(chunk),
333                        globals: interp.globals_snapshot().clone(),
334                    };
335                    self.stack.push(Value::Foreign(Arc::new(compiled)));
336                }
337
338                Op::Call(arity) => {
339                    self.do_call(chunk, interp, host, arity, span, /*tail=*/ false)?;
340                }
341                Op::TailCall(arity) => {
342                    self.do_call(chunk, interp, host, arity, span, /*tail=*/ true)?;
343                }
344                Op::Return => {
345                    let ret = self.stack.pop().ok_or(VmError::Underflow { ip: 0 })?;
346                    // Drop locals for this frame.
347                    let f = self
348                        .frames
349                        .pop()
350                        .expect("Return with no active frame");
351                    self.stack.truncate(f.locals_base);
352                    if self.frames.is_empty() {
353                        return Ok(ret);
354                    }
355                    self.stack.push(ret);
356                }
357
358                Op::MakeList(n) => {
359                    let len = self.stack.len();
360                    if n > len {
361                        return Err(VmError::Underflow { ip: 0 });
362                    }
363                    let items: Vec<Value> = self.stack.drain(len - n..).collect();
364                    self.stack.push(Value::list(items));
365                }
366
367                Op::EvalSexp(idx) => {
368                    // Tree-walker fallback. The const pool entry is
369                    // a `Value::Sexp(Sexp, Span)` (set up by the
370                    // compiler's emit_eval_sexp). Lift back to a
371                    // Spanned and call into the host interpreter.
372                    let v = chunk.consts.get(idx).clone();
373                    let (sexp, sp) = match v {
374                        Value::Sexp(s, sp) => (s, sp),
375                        _ => {
376                            return Err(VmError::Eval(crate::error::EvalError::native_fn(
377                                Arc::<str>::from("vm:eval-sexp"),
378                                "expected a Sexp constant in EvalSexp",
379                                span,
380                            )));
381                        }
382                    };
383                    let spanned = tatara_lisp::Spanned::from_sexp_at(&sexp, sp);
384                    let result = interp.eval_spanned(&spanned, host)?;
385                    self.stack.push(result);
386                }
387
388                Op::PushHandler {
389                    catch_ip,
390                    error_local,
391                } => {
392                    let stack_at_push = self.stack.len();
393                    self.frames[frame_idx].handlers.push(HandlerRecord {
394                        catch_ip,
395                        error_local,
396                        stack_at_push,
397                    });
398                }
399                Op::PopHandler => {
400                    self.frames[frame_idx].handlers.pop();
401                }
402            }
403        }
404    }
405
406    /// Wrap the raw run loop so any propagating error gets routed
407    /// through the nearest installed handler. Frames above the
408    /// handler are unwound; the handler's frame jumps to its
409    /// `catch_ip` with the error value stored at `error_local`.
410    fn run_with_handlers<H: 'static>(
411        &mut self,
412        chunk: &Arc<Chunk>,
413        interp: &mut Interpreter<H>,
414        host: &mut H,
415    ) -> Result<Value, VmError> {
416        loop {
417            match self.run_inner(chunk, interp, host) {
418                Ok(v) => return Ok(v),
419                Err(VmError::Eval(eval_err)) => {
420                    let err_value = vm_err_to_value(&eval_err);
421                    if !self.unwind_to_handler(err_value) {
422                        return Err(VmError::Eval(eval_err));
423                    }
424                }
425                Err(other) => {
426                    let err_value = vm_runtime_err_to_value(&other);
427                    if !self.unwind_to_handler(err_value) {
428                        return Err(other);
429                    }
430                }
431            }
432        }
433    }
434
435    /// Find the nearest frame with at least one installed handler
436    /// and jump to it. Pops every frame above; truncates the value
437    /// stack to the handler's snapshot; stores `err_value` into the
438    /// handler's `error_local`; sets the handler-frame's IP to
439    /// `catch_ip`. Returns `false` if no handler is installed
440    /// anywhere — caller propagates the error to the embedder.
441    fn unwind_to_handler(&mut self, err_value: Value) -> bool {
442        // Walk frames innermost → outermost looking for a handler.
443        for frame_idx in (0..self.frames.len()).rev() {
444            if !self.frames[frame_idx].handlers.is_empty() {
445                // Pop every frame above this one.
446                while self.frames.len() > frame_idx + 1 {
447                    let f = self.frames.pop().unwrap();
448                    self.stack.truncate(f.locals_base);
449                }
450                // Pop the most recent handler from THIS frame.
451                let handler = self.frames[frame_idx]
452                    .handlers
453                    .pop()
454                    .expect("handler present");
455                // Truncate the value stack to whatever it was at
456                // PushHandler time so handler logic starts clean.
457                self.stack.truncate(handler.stack_at_push);
458                // Store the error value into the handler's local slot.
459                let abs = self.frames[frame_idx].locals_base + handler.error_local;
460                // The slot may have been promoted to a cell.
461                if let Some(cell) = self.frames[frame_idx]
462                    .local_cells
463                    .get(&handler.error_local)
464                    .cloned()
465                {
466                    *cell.lock().unwrap() = err_value;
467                } else if abs < self.stack.len() {
468                    self.stack[abs] = err_value;
469                } else {
470                    // Grow the stack with nils until we can write.
471                    while self.stack.len() <= abs {
472                        self.stack.push(Value::Nil);
473                    }
474                    self.stack[abs] = err_value;
475                }
476                // Resume at the handler.
477                self.frames[frame_idx].ip = handler.catch_ip;
478                return true;
479            }
480        }
481        false
482    }
483
484    fn pop_or_nil(&mut self) -> Value {
485        self.stack.pop().unwrap_or(Value::Nil)
486    }
487
488    fn lookup_global<H: 'static>(
489        &self,
490        interp: &Interpreter<H>,
491        name: &str,
492    ) -> Option<Value> {
493        interp.lookup_global(name)
494    }
495
496    fn do_call<H: 'static>(
497        &mut self,
498        _chunk: &Chunk,
499        interp: &mut Interpreter<H>,
500        host: &mut H,
501        arity: usize,
502        span: Span,
503        tail: bool,
504    ) -> Result<(), VmError> {
505        let stack_len = self.stack.len();
506        if stack_len < arity + 1 {
507            return Err(VmError::Underflow { ip: 0 });
508        }
509        let callee_idx = stack_len - arity - 1;
510        let callee = self.stack[callee_idx].clone();
511
512        // At the top level, TailCall is structurally identical to
513        // Call — there's no enclosing frame to fold into. Detect by
514        // depth and downgrade so the rest of the dispatch is uniform.
515        let tail = tail && self.frames.len() > 1;
516
517        // Branch on callee kind.
518        match &callee {
519            // VM-compiled closure (Foreign-tagged CompiledClosure).
520            Value::Foreign(any) => {
521                if let Ok(cc) = any.clone().downcast::<CompiledClosure>() {
522                    return self.invoke_compiled(cc, arity, span, tail);
523                }
524                // Other Foreign values aren't callable.
525                Err(VmError::NotCallable {
526                    kind: callee.type_name(),
527                    at: span,
528                })
529            }
530            // Native or tree-walker closure — go through the eval
531            // crate's apply path. This is what makes every primitive
532            // and every dynamically-loaded closure work uniformly.
533            Value::NativeFn(_) | Value::Closure(_) => {
534                // Drain args from the stack, then drop the callee
535                // slot so nothing's left in callee_idx.
536                let args: Vec<Value> = self.stack.drain(callee_idx + 1..).collect();
537                self.stack.pop();
538                let result = interp.apply_external_value(&callee, args, host, span)?;
539                self.stack.push(result);
540                Ok(())
541            }
542            other => Err(VmError::NotCallable {
543                kind: other.type_name(),
544                at: span,
545            }),
546        }
547    }
548
549    fn invoke_compiled(
550        &mut self,
551        cc: Arc<CompiledClosure>,
552        arity: usize,
553        span: Span,
554        tail: bool,
555    ) -> Result<(), VmError> {
556        let body = cc.body.clone();
557        let required = body.params.len();
558        let has_rest = body.rest.is_some();
559        if !has_rest && arity != required {
560            return Err(VmError::Arity {
561                expected: required,
562                got: arity,
563                at: span,
564            });
565        }
566        if has_rest && arity < required {
567            return Err(VmError::Arity {
568                expected: required,
569                got: arity,
570                at: span,
571            });
572        }
573
574        // Pop args from stack.
575        let stack_len = self.stack.len();
576        let args_start = stack_len - arity;
577        let args: Vec<Value> = self.stack.drain(args_start..).collect();
578        // Pop the callee.
579        self.stack.pop();
580
581        // Build the new locals layout.
582        let mut locals: Vec<Value> = Vec::with_capacity(body.locals);
583        for v in args.iter().take(required) {
584            locals.push(v.clone());
585        }
586        if let Some(_) = &body.rest {
587            let rest_args: Vec<Value> = args.iter().skip(required).cloned().collect();
588            locals.push(Value::list(rest_args));
589        }
590        while locals.len() < body.locals {
591            locals.push(Value::Nil);
592        }
593
594        if tail && !self.frames.is_empty() {
595            // Reuse the current frame: drop its locals, push new ones.
596            let frame_idx = self.frames.len() - 1;
597            let f = &mut self.frames[frame_idx];
598            self.stack.truncate(f.locals_base);
599            for v in locals {
600                self.stack.push(v);
601            }
602            f.func = body.clone();
603            f.ip = 0;
604            f.captures = cc.captures.clone();
605            f.local_cells.clear();
606            f.handlers.clear();
607            // stack_base relative to locals_base stays the same.
608            f.stack_base = f.locals_base + body.locals;
609        } else {
610            // Push a new frame.
611            let locals_base = self.stack.len();
612            for v in locals {
613                self.stack.push(v);
614            }
615            let stack_base = self.stack.len();
616            self.frames.push(Frame {
617                func: body.clone(),
618                ip: 0,
619                locals_base,
620                stack_base,
621                captures: cc.captures.clone(),
622                local_cells: std::collections::HashMap::new(),
623                handlers: Vec::new(),
624            });
625        }
626        Ok(())
627    }
628}
629
630impl Default for Vm {
631    fn default() -> Self {
632        Self::new()
633    }
634}
635
636/// Convert an `EvalError` into a `Value::Error` so a try/catch
637/// handler can observe Rust-side errors uniformly with user-thrown
638/// ones. User-thrown errors (carried in `EvalError::User`) preserve
639/// their original `Value` for transparency.
640fn vm_err_to_value(err: &crate::error::EvalError) -> Value {
641    use crate::error::EvalError::*;
642    if let User { value, .. } = err {
643        return value.clone();
644    }
645    let tag: Arc<str> = match err {
646        UnboundSymbol { .. } => Arc::from("unbound-symbol"),
647        ArityMismatch { .. } => Arc::from("arity-mismatch"),
648        TypeMismatch { .. } => Arc::from("type-mismatch"),
649        DivisionByZero { .. } => Arc::from("division-by-zero"),
650        NotCallable { .. } => Arc::from("not-callable"),
651        BadSpecialForm { .. } => Arc::from("bad-special-form"),
652        NativeFn { .. } => Arc::from("native-fn"),
653        Reader(_) => Arc::from("reader"),
654        Halted => Arc::from("halted"),
655        NotImplemented(_) => Arc::from("not-implemented"),
656        User { .. } => unreachable!(),
657    };
658    Value::Error(Arc::new(crate::value::ErrorObj {
659        tag,
660        message: Arc::from(err.short_message()),
661        data: Vec::new(),
662    }))
663}
664
665/// Convert a non-EvalError VM error (Underflow, BadLocal, Unbound,
666/// NotCallable, Arity) into a `Value::Error` for handler routing.
667fn vm_runtime_err_to_value(err: &VmError) -> Value {
668    let (tag, message): (&str, String) = match err {
669        VmError::Underflow { ip } => ("vm-underflow", format!("stack underflow at op {ip}")),
670        VmError::Unbound { name, .. } => ("unbound-symbol", format!("unbound symbol `{name}`")),
671        VmError::NotCallable { kind, .. } => {
672            ("not-callable", format!("value of type {kind} is not callable"))
673        }
674        VmError::Arity { expected, got, .. } => (
675            "arity-mismatch",
676            format!("expected {expected} args, got {got}"),
677        ),
678        VmError::BadLocal(idx) => ("bad-local", format!("local index out of bounds: {idx}")),
679        VmError::Eval(inner) => return vm_err_to_value(inner),
680    };
681    Value::Error(Arc::new(crate::value::ErrorObj {
682        tag: Arc::from(tag),
683        message: Arc::from(message),
684        data: Vec::new(),
685    }))
686}
687
688/// Foreign-tagged compiled closure — the VM's native callable shape.
689/// Wrapping in `Foreign` lets us pass it through `Value` (which is
690/// shared with the tree-walker) without growing the `Value` enum.
691///
692/// A `CompiledClosure` is **self-contained**: it carries everything
693/// needed to re-invoke the body in isolation:
694///   - the compiled body (`body`);
695///   - one upvalue cell per free variable (`captures`);
696///   - the enclosing chunk (`chunk`) so opcode operands referencing
697///     `consts` / `names` / `fn_table` resolve correctly;
698///   - a snapshot of the host's globals env at MakeClosure time
699///     (`globals`). `Env` is cheap to clone — frames are shared via
700///     `Arc<Mutex<...>>`, so subsequent global definitions on the
701///     host interpreter are still visible through this snapshot.
702///
703/// The self-contained shape is what lets a `Value::Foreign(CompiledClosure)`
704/// flow into a native higher-order primitive (`map`, `filter`, ...) and
705/// be invoked through `Caller::apply_value` — the apply path can spin
706/// up a fresh `Vm` against just the closure + a `&mut H` host without
707/// needing a re-entrant `&mut Interpreter` borrow.
708#[derive(Clone)]
709pub struct CompiledClosure {
710    pub body: Arc<CompiledFn>,
711    pub captures: Vec<Arc<Mutex<Value>>>,
712    pub chunk: Arc<super::chunk::Chunk>,
713    pub globals: crate::env::Env,
714}
715
716impl CompiledClosure {
717    /// Lift this VM-compiled closure to a tree-walker-shaped
718    /// `crate::value::Closure`. Used when a native higher-order
719    /// primitive (`map`, `filter`, `foldl`, ...) holds a
720    /// `Value::Foreign(CompiledClosure)` and needs to invoke it
721    /// through the standard `Caller::apply_value` path — that path
722    /// goes through `eval::apply()` which knows how to dispatch
723    /// `Value::Closure`.
724    ///
725    /// The lifted closure carries:
726    ///   - the original Spanned body (preserved by the compiler in
727    ///     `body.source_body`);
728    ///   - a `captured_env` synthesized from the closure's positional
729    ///     captures + the host globals snapshot.
730    ///
731    /// Trade-off: the lifted invocation runs through the tree-walker,
732    /// not the VM. Faster paths (direct VM dispatch) are possible but
733    /// would require threading mutable Interpreter state through
734    /// `Caller`. Correctness-wise the tree-walker is authoritative —
735    /// the VM is parity-validated against it.
736    ///
737    /// Mutation note: `set!` performed inside the lifted closure
738    /// writes to the lifted `captured_env`, NOT to the original
739    /// upvalue cells. For HoF callbacks this is the common case
740    /// (read-only captures); closures that need shared `set!`
741    /// semantics should be invoked through the VM directly.
742    pub fn lift_to_closure(&self) -> Arc<crate::value::Closure> {
743        let mut captured_env = self.globals.clone();
744        captured_env.push();
745        for ((name, _), cell) in self.body.captures.iter().zip(self.captures.iter()) {
746            let v = cell.lock().unwrap().clone();
747            captured_env.define(name.clone(), v);
748        }
749        Arc::new(crate::value::Closure {
750            params: self.body.params.clone(),
751            rest: self.body.rest.clone(),
752            body: self.body.source_body.clone(),
753            captured_env,
754            source: self.body.source_span,
755        })
756    }
757}
758
759impl std::fmt::Debug for CompiledClosure {
760    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
761        f.debug_struct("CompiledClosure")
762            .field("params", &self.body.params)
763            .field("ops_len", &self.body.ops.len())
764            .field("captures", &self.captures.len())
765            .finish()
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772    use crate::Interpreter;
773    use crate::install_full_stdlib_with;
774    use crate::vm::compile::compile_program;
775    use tatara_lisp::read_spanned;
776
777    struct NoHost;
778
779    /// Helper: read + compile + run via VM, return final Value.
780    fn run_vm(src: &str) -> Value {
781        let mut i: Interpreter<NoHost> = Interpreter::new();
782        install_full_stdlib_with(&mut i, &mut NoHost);
783        let forms = read_spanned(src).unwrap();
784        // Macroexpand first so the VM never sees defmacro-introduced
785        // syntax. The expander mutates `i.expander` from any
786        // top-level (defmacro …) forms in the source.
787        let mut expanded: Vec<tatara_lisp::Spanned> = Vec::new();
788        for form in &forms {
789            if i.expander_mut().try_register_macro(form).unwrap() {
790                continue;
791            }
792            expanded.push(i.fully_expand(form, &mut NoHost).unwrap());
793        }
794        let chunk = compile_program(&expanded).unwrap();
795        let mut vm = Vm::new();
796        vm.run(&chunk, &mut i, &mut NoHost).unwrap()
797    }
798
799    #[test]
800    fn run_int_literal() {
801        assert!(matches!(run_vm("42"), Value::Int(42)));
802    }
803
804    #[test]
805    fn run_arithmetic_via_native_add() {
806        assert!(matches!(run_vm("(+ 1 2 3)"), Value::Int(6)));
807    }
808
809    #[test]
810    fn run_if_picks_branch() {
811        assert!(matches!(run_vm("(if #t 100 200)"), Value::Int(100)));
812        assert!(matches!(run_vm("(if #f 100 200)"), Value::Int(200)));
813    }
814
815    #[test]
816    fn run_let_binds_and_uses() {
817        assert!(matches!(
818            run_vm("(let ((x 10) (y 20)) (+ x y))"),
819            Value::Int(30)
820        ));
821    }
822
823    #[test]
824    fn run_define_then_use() {
825        assert!(matches!(run_vm("(define x 99) x"), Value::Int(99)));
826    }
827
828    #[test]
829    fn run_define_function_shorthand() {
830        assert!(matches!(
831            run_vm("(define (sq x) (* x x)) (sq 7)"),
832            Value::Int(49)
833        ));
834    }
835
836    #[test]
837    fn run_lambda_inline_application() {
838        assert!(matches!(
839            run_vm("((lambda (x y) (+ x y)) 3 4)"),
840            Value::Int(7)
841        ));
842    }
843
844    #[test]
845    fn run_recursion_via_global_define() {
846        let v = run_vm(
847            "(define (fact n)
848               (if (= n 0) 1 (* n (fact (- n 1)))))
849             (fact 6)",
850        );
851        assert!(matches!(v, Value::Int(720)));
852    }
853
854    #[test]
855    fn run_begin_returns_last() {
856        assert!(matches!(run_vm("(begin 1 2 3)"), Value::Int(3)));
857    }
858
859    #[test]
860    fn run_and_short_circuits() {
861        // (and #t 5) → 5 (last truthy wins).
862        assert!(matches!(run_vm("(and #t 5)"), Value::Int(5)));
863        assert!(matches!(run_vm("(and #f 5)"), Value::Bool(false)));
864    }
865
866    #[test]
867    fn run_or_short_circuits() {
868        assert!(matches!(run_vm("(or #f 7)"), Value::Int(7)));
869        assert!(matches!(run_vm("(or #f #f)"), Value::Bool(false)));
870    }
871
872    #[test]
873    fn run_not_inverts() {
874        assert!(matches!(run_vm("(not #t)"), Value::Bool(false)));
875        assert!(matches!(run_vm("(not #f)"), Value::Bool(true)));
876    }
877
878    #[test]
879    fn run_quoted_symbol_passes_through() {
880        let v = run_vm("'foo");
881        assert!(matches!(v, Value::Symbol(s) if &*s == "foo"));
882    }
883
884    #[test]
885    fn run_set_mutates_global() {
886        assert!(matches!(
887            run_vm("(define x 1) (set! x 99) x"),
888            Value::Int(99)
889        ));
890    }
891
892    #[test]
893    fn run_tail_call_loops_in_constant_space() {
894        // Tail-call optimized recursion. Without TCO this would
895        // stack-overflow at ~10k frames; with TCO it runs in O(1)
896        // stack space. Only test 50_000 iterations to keep this
897        // CI-fast — the principle is proved.
898        let v = run_vm(
899            "(define (loop n) (if (= n 0) :done (loop (- n 1))))
900             (loop 50000)",
901        );
902        assert!(matches!(v, Value::Keyword(s) if &*s == "done"));
903    }
904
905    // ── VM Phase 3: closure capture of outer locals ───────────────
906
907    #[test]
908    fn closure_captures_outer_let_local() {
909        // (let ((x 10)) ((lambda (y) (+ x y)) 5)) → 15
910        // Without capture-aware compilation this would fail with
911        // "unbound symbol x" in the lambda body.
912        let v = run_vm("(let ((x 10)) ((lambda (y) (+ x y)) 5))");
913        assert!(matches!(v, Value::Int(15)));
914    }
915
916    #[test]
917    fn closure_returned_from_let_still_sees_captured() {
918        // Classic make-adder pattern: closure outlives the
919        // enclosing scope. Captures must be by-cell (Arc<Mutex>),
920        // not by-frame-position, so the lambda still resolves x
921        // after the let frame is gone.
922        let v = run_vm(
923            "(define (make-adder n) (lambda (x) (+ x n)))
924             ((make-adder 10) 32)",
925        );
926        assert!(matches!(v, Value::Int(42)));
927    }
928
929    #[test]
930    fn nested_closures_chain_captures() {
931        // (let ((x 5))
932        //   (let ((f (lambda (a) (lambda (b) (+ x a b)))))
933        //     ((f 3) 4)))
934        // → 5 + 3 + 4 = 12. The inner lambda captures x via the
935        // outer lambda's captures (chained), and a directly from the
936        // outer lambda's locals.
937        let v = run_vm(
938            "(let ((x 5))
939               (let ((f (lambda (a) (lambda (b) (+ x a b)))))
940                 ((f 3) 4)))",
941        );
942        assert!(matches!(v, Value::Int(12)));
943    }
944
945    // ── VM Phase 4: try / catch ────────────────────────────────────
946
947    #[test]
948    fn try_returns_body_value_when_no_throw() {
949        let v = run_vm(
950            "(try
951               (+ 1 2 3)
952               (catch (e) :unreachable))",
953        );
954        assert!(matches!(v, Value::Int(6)));
955    }
956
957    #[test]
958    fn try_catches_user_throw() {
959        let v = run_vm(
960            "(try
961               (throw (ex-info \"boom\" (list)))
962               (catch (e) (error-message e)))",
963        );
964        match v {
965            Value::Str(s) => assert_eq!(&*s, "boom"),
966            other => panic!("{other:?}"),
967        }
968    }
969
970    #[test]
971    fn try_catches_runtime_error() {
972        // Type mismatch from a primitive — Rust-side error, not user
973        // throw. The VM converts it to a Value::Error and routes
974        // through the handler.
975        let v = run_vm(
976            "(try
977               (+ 1 \"oops\")
978               (catch (e) (error-tag e)))",
979        );
980        // type-mismatch is the canonical tag for type errors.
981        match v {
982            Value::Keyword(s) => assert_eq!(&*s, "type-mismatch"),
983            other => panic!("{other:?}"),
984        }
985    }
986
987    #[test]
988    fn try_inside_a_function_body() {
989        // The try frame is the lambda's frame, not the top-level.
990        // Verifies handler unwinding when the inner frame is the
991        // one with the handler installed.
992        let v = run_vm(
993            "(define (safe-div a b)
994               (try
995                 (/ a b)
996                 (catch (e) :div-failed)))
997             (safe-div 10 0)",
998        );
999        assert!(matches!(v, Value::Keyword(s) if &*s == "div-failed"));
1000    }
1001
1002    #[test]
1003    fn nested_try_inner_catches_first() {
1004        let v = run_vm(
1005            "(try
1006               (try
1007                 (throw (ex-info \"inner\" (list)))
1008                 (catch (e) :inner-caught))
1009               (catch (e) :outer-caught))",
1010        );
1011        assert!(matches!(v, Value::Keyword(s) if &*s == "inner-caught"));
1012    }
1013
1014    // ── VM Phase 5: tree-walker fallback ───────────────────────────
1015
1016    #[test]
1017    fn vm_falls_back_to_tree_walker_for_quasi_quote() {
1018        // Quasi-quote isn't a VM opcode; the compiler emits EvalSexp,
1019        // which dispatches the form through Interpreter::eval_spanned
1020        // at runtime. The dispatch only sees globals (not VM locals)
1021        // — that's an acknowledged limitation of EvalSexp fallback.
1022        // Use a (define) so x is a global the tree-walker can see.
1023        let v = run_vm("(define x 99) `(a ,x c)");
1024        match v {
1025            Value::List(xs) => {
1026                assert_eq!(xs.len(), 3);
1027                assert!(matches!(&xs[1], Value::Int(99)));
1028            }
1029            other => panic!("{other:?}"),
1030        }
1031    }
1032
1033    #[test]
1034    fn vm_falls_back_for_eval() {
1035        // (eval '(+ 1 2 3)) — runtime metaprogramming, falls back.
1036        let v = run_vm("(eval '(+ 1 2 3))");
1037        assert!(matches!(v, Value::Int(6)));
1038    }
1039
1040    #[test]
1041    fn vm_falls_back_for_macroexpand() {
1042        // The macroexpand introspection special form is in the
1043        // fallback list; the VM defers to the tree-walker.
1044        let v = run_vm(
1045            "(defmacro twice (x) `(* ,x 2))
1046             (macroexpand-1 '(twice 7))",
1047        );
1048        // (twice 7) → (* 7 2) — list-shape with three symbol/int
1049        // elements.
1050        match v {
1051            Value::List(xs) => assert_eq!(xs.len(), 3),
1052            other => panic!("{other:?}"),
1053        }
1054    }
1055
1056    #[test]
1057    fn try_handler_can_rethrow_to_outer() {
1058        let v = run_vm(
1059            "(try
1060               (try
1061                 (throw (ex-info \"first\" (list)))
1062                 (catch (e) (throw (ex-info \"rethrown\" (list)))))
1063               (catch (e) (error-message e)))",
1064        );
1065        match v {
1066            Value::Str(s) => assert_eq!(&*s, "rethrown"),
1067            other => panic!("{other:?}"),
1068        }
1069    }
1070
1071    #[test]
1072    fn closure_set_on_captured_propagates() {
1073        // Two closures sharing the same outer binding. Setting x
1074        // through one should be visible through the other (the
1075        // captures are by-reference cells).
1076        let v = run_vm(
1077            "(define get (let ((x 0))
1078                           (define setter (lambda (v) (set! x v)))
1079                           (define getter (lambda () x))
1080                           (setter 42)
1081                           getter))
1082             (get)",
1083        );
1084        assert!(matches!(v, Value::Int(42)));
1085    }
1086}