narju/eval/
machine.rs

1//! CK machine — explicit-continuation iterative evaluator.
2//!
3//! ## Overview
4//!
5//! This is `narju`'s sole evaluator. It walks an `Exp` tree by alternating
6//! between two modes:
7//!
8//!   - `Mode::Eval { env, exp }` — about to evaluate `exp` under `env`.
9//!     `step_eval` dispatches on `exp` and either produces a `Val`
10//!     immediately (terminals) or pushes a `Cont` onto the kont stack
11//!     and switches to a sub-expression.
12//!
13//!   - `Mode::Apply { val }` — just produced `val`. Pop a `Cont` from
14//!     the kont stack; `step_apply` resumes the paused operation,
15//!     either producing another val or switching back to `Mode::Eval`.
16//!
17//! Every operand evaluation, every reify scope, every η-expansion lives
18//! on the heap kont rather than the Rust stack. The Rust stack depth is
19//! O(1) regardless of program size or staging tower depth. (The name is
20//! Felleisen's: C for the control string, K for the continuation stack.
21//! Environments ride inside both, so strictly this is a CEK machine
22//! with E folded into C and K.)
23//!
24//! ## Where staging happens
25//!
26//! The machine does not distinguish "normal" from "staging" evaluation.
27//! Staged operations are just the arms of each dispatch that fire when
28//! an operand is `Val::Code`: instead of computing, they call
29//! `EvalCtx::reflectc`, which appends the operation to the innermost
30//! open block in the [`EvalCtx`] and hands back a fresh residual
31//! variable. Reify scopes (Class S below) delimit those blocks and fold
32//! them into ANF Let-chains. All state involved lives in the `EvalCtx`;
33//! `ScopeSnapshot` saves and restores it around each scope — except
34//! cells and I/O, which are effects and survive.
35//!
36//! ## Layout of this file
37//!
38//! In source order: [`Mode`]; the operator-kind enums ([`BinOpKind`],
39//! [`UnOpKind`]); [`Cont`], the continuation alphabet itself; the
40//! resume-descriptor enums ([`ForceMode`], [`ForceCodeResume`],
41//! [`ReifyResume`]); [`Machine`] and its run loop with
42//! `unwind_to_catch`; `step_eval`; `step_apply`; the shared dispatch
43//! helpers (`dispatch_unop`, `dispatch_binop`); and [`ScopeSnapshot`]
44//! at the end.
45//!
46//! ## Continuation taxonomy
47//!
48//!   - **Class T (tail).** `Let` body, `If` chosen-branch (Cst), `App`
49//!     closure body. Resumes a `Mode::Eval` directly — no kont frame.
50//!
51//!   - **Class O (operand).** Binary/unary primitives push a cont
52//!     holding the unevaluated sibling (binary) or the op-kind (unary),
53//!     then evaluate the first/only operand. Dispatch happens on
54//!     resume in `step_apply`.
55//!
56//!   - **Class S (staged).** Reify scopes (`If`-Code, `LiftRef`-Code,
57//!     `Run`-Code, run-now, `Evalms`) push `Cont::ForceCode { resume:
58//!     ReifyDrain { saved, then } }` or `Cont::ReifyVExit { saved,
59//!     resume }`, save a `ScopeSnapshot`, and evaluate the staged body
60//!     under a fresh block. On completion the kont drains `ctx.block`
61//!     into a Let-chain and dispatches a `ReifyResume` to describe what
62//!     surrounding staged construction is being assembled.
63//!
64//!     Lift's `Clo` arm η-expansion is implemented as a nested
65//!     `ForceCode` → `LiftCloFinish` pair that grows the kont rather
66//!     than the Rust stack.
67//!
68//!   - **Class C (error).** `Catch` pushes `Cont::CatchExit { saved }`
69//!     and evaluates the body. The Machine's run loop catches `Err`
70//!     results from `step_eval`/`step_apply` and walks the kont via
71//!     `unwind_to_catch`, restoring state from each popped cont until
72//!     it finds a `CatchExit` (or runs out, in which case the original
73//!     error propagates).
74
75use std::rc::Rc;
76
77use crate::core::{Env, Exp, RcExp, Val};
78
79use super::{EvalCtx, FunRegistry};
80
81// ── Mode ──────────────────────────────────────────────────────────────────────
82
83/// The CK machine alternates between two modes:
84///
85///   - `Eval`: we have an `Exp` to evaluate under `Env`. The dispatcher
86///     `step_eval` inspects the `Exp` and either produces a `Val`
87///     immediately (literals, vars, lambdas) or pushes a `Cont` and
88///     switches to evaluating a sub-expression.
89///
90///   - `Apply`: we have a finished `Val`. The dispatcher pops the topmost
91///     `Cont` from the kont stack and integrates the `Val` into whatever
92///     pending operation that continuation represents — possibly producing
93///     another `Eval` step (for the next operand), another `Apply` step
94///     (the operation produced a final value), or terminating the loop
95///     (kont stack empty).
96pub enum Mode {
97    /// Evaluate `exp` under `env`. The result is fed to whatever
98    /// continuation is on top of the stack.
99    Eval {
100        /// Environment to evaluate under.
101        env: Env,
102        /// Expression to evaluate.
103        exp: RcExp,
104    },
105
106    /// Apply the topmost continuation to `val`. If the stack is empty,
107    /// `val` is the final result.
108    Apply {
109        /// The value just produced.
110        val: Val,
111    },
112}
113
114// ── Operator kinds ────────────────────────────────────────────────────────────
115//
116// Class O dispatch is centralised through these enums so the CK machine's
117// `Cont` enum doesn't need a variant per Exp::Plus / Exp::Minus / etc.
118
119/// Binary primitive operators that share the same shape:
120///   - eval e1 → v1
121///   - eval e2 → v2
122///   - dispatch on (v1, v2)
123///
124/// The dispatch differs per op (arithmetic vs equality vs cons-build),
125/// but the operand sequencing is identical.
126///
127/// **Not in this enum:** `LiftRef`. Although LiftRef takes two Exps,
128/// its dispatch on `v1` happens *before* `e2` is evaluated — if `v1` is
129/// Code, `e2` must be evaluated inside a reify scope, not normally.
130/// LiftRef has its own dedicated continuations (`Cont::LiftRefDispatch`,
131/// `Cont::LiftRefForceCode`).
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum BinOpKind {
134    /// `+` — Cst+Cst → Cst, Code+Code → reflect Plus, else TypeError
135    Plus,
136    /// `-` — Cst−Cst → Cst, Code−Code → reflect Minus, else TypeError
137    Minus,
138    /// `*` — Cst×Cst → Cst, Code×Code → reflect Times, else TypeError
139    Times,
140    /// `eq?` — any pair → Cst(0|1), Code+Code → reflect Eq
141    Eq,
142    /// `cell-set!` — Cell×any → write (returns written value),
143    /// Code×Code → reflect CellSet, else TypeError
144    CellSet,
145    /// `cons` — any → Tup, never staged (pairs always run-time data).
146    ///
147    /// Cons lives in `BinOpKind` to reuse the `BinOpRight`/`BinOpFinish`
148    /// operand-sequencing machinery — it's the only variant here that
149    /// has no Code-dispatch branch and is not stage-polymorphic at all.
150    /// The dispatcher just unconditionally builds a `Val::Tup`; stage
151    /// reflection for `cons` happens one level up, when a `Val::Tup` of
152    /// two `Val::Code` is fed to `lift` (see `eval/mod.rs::lift`'s Tup
153    /// arm). Resist the urge to add a fake Code arm to `dispatch_binop`
154    /// for symmetry — the paper's `λ↑↓` does not stage cons directly.
155    Cons,
156    /// `code?` — Cst(0|1) on second arg's variant; if first is Code,
157    /// reflect IsCode (second must then be Code or TypeError)
158    IsCode,
159    /// `log` — first=Code → stage the log; else IO-print second.
160    /// Both operands are evaluated regardless of the dispatch (unlike
161    /// LiftRef), so the standard binop sequencing fits.
162    Log,
163    /// Function application `(f x)` — Clo+_ → tail-jump body; Code+Code →
164    /// reflect App; else TypeError. This is the only binop where one
165    /// outcome is `Mode::Eval` (tail jump) rather than a pure Val
166    /// producer; App is therefore special-cased in `step_apply` and
167    /// not dispatched through `dispatch_binop`.
168    App,
169}
170
171/// Unary primitive operators that share the same shape:
172///   - eval e1 → v1
173///   - dispatch on v1
174///
175/// `Run`'s b-operand is also unary in this sense — once b is evaluated,
176/// the Run arm decides between staged-run (b is Code) and run-now
177/// (b is anything else). The `e` operand is held on the continuation.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum UnOpKind {
180    /// `num?` — Cst → 1, Code → reflect IsNum, else 0
181    IsNum,
182    /// `sym?` — Sym → 1, Code → reflect IsSym, else 0
183    IsSym,
184    /// `nil?` — Nil → 1, Code → reflect IsNil, else 0
185    IsNil,
186    /// `pair?` — Tup → 1, Code → reflect IsPair, else 0
187    IsPair,
188    /// `car` — Tup → first, Code → reflect Car, else TypeError
189    Car,
190    /// `cdr` — Tup → second, Code → reflect Cdr, else TypeError
191    Cdr,
192    /// `cell-new` — any non-Code → allocate Cell, Code → reflect CellNew
193    CellNew,
194    /// `cell-read` — Cell → contents, Code → reflect CellRead, else TypeError
195    CellRead,
196    /// `lift` — wrap as Code via the `lift` operator (Cst/Sym/Nil are
197    /// trivial; Clo triggers η-expansion; Tup recurses on parts;
198    /// Code lifts one stage).
199    Lift,
200    /// `print` — IO side effect, returns Nil
201    Print,
202}
203
204// ── Continuation stack ────────────────────────────────────────────────────────
205
206/// An entry on the kont stack — represents pending work to resume when the
207/// current sub-evaluation produces a `Val`.
208///
209/// Each variant either:
210///   - Holds onto an unevaluated subexpression that becomes the next
211///     `Mode::Eval` (operand sequencing), or
212///   - Holds enough state to dispatch on the resumed Val and either
213///     produce a final Val (Class O), or
214///   - Holds a `ScopeSnapshot` for restoring staging state on exit
215///     from a reify-like region (Class S).
216pub enum Cont {
217    // ── Class O — operand sequencing for binary forms ──────────────────────────
218
219    /// We just evaluated the first operand; resumed `Val` becomes `v1`.
220    /// We need to evaluate `e2` next, then dispatch via `BinOpKind`.
221    /// Pushes `BinOpFinish { op, v1 }` on resume.
222    BinOpRight {
223        /// Environment for evaluating `e2`.
224        env: Env,
225        /// Which binary form this is.
226        op: BinOpKind,
227        /// The unevaluated second operand.
228        e2: RcExp,
229    },
230
231    /// Both operands have been evaluated. Resumed `Val` is `v2`.
232    /// Dispatch on `(v1, v2)` per `BinOpKind` semantics.
233    BinOpFinish {
234        /// Which binary form this is.
235        op: BinOpKind,
236        /// The already-evaluated first operand.
237        v1: Val,
238    },
239
240    // ── Class O — operand sequencing for unary forms ──────────────────────────
241
242    /// We just evaluated the operand. Dispatch on the resumed Val.
243    UnOpFinish {
244        /// Which unary form this is.
245        op: UnOpKind,
246    },
247
248    // ── Class O — `If` (three children) ────────────────────────────────────────
249
250    /// We evaluated `c`. If Cst, tail-evaluate the chosen branch.
251    /// If Code, push a reify scope to compile both branches and reflect.
252    /// Else TypeError.
253    IfDispatch {
254        /// Environment for evaluating the chosen branch (or both,
255        /// staged).
256        env: Env,
257        /// The unevaluated true branch.
258        t: RcExp,
259        /// The unevaluated false branch.
260        f: RcExp,
261    },
262
263    // ── Class O / T — `Let` (two children) ─────────────────────────────────────
264
265    /// We evaluated `e1`; the resumed `Val` becomes the new top of the
266    /// environment, and we tail-step into `e2`. This is operand eval on
267    /// the kont (Class O) followed by a tail step (Class T) — Let is a
268    /// hybrid form whose flow class differs across its two operands.
269    LetBody {
270        /// Environment the bound value is pushed onto.
271        env: Env,
272        /// The unevaluated body.
273        e2: RcExp,
274    },
275
276    // ── Class O — `LiftRef` (two children with v1-dependent e2 evaluation) ─────
277
278    /// We evaluated `e1`. LiftRef's dispatch on `v1` decides *how* to
279    /// evaluate `e2` — staged inside a reify scope (if `v1` is Code), or
280    /// normally followed by cross-stage persistence (otherwise). This
281    /// differs from the standard binop shape (which always evaluates `e2`
282    /// normally), so LiftRef has its own dedicated continuation.
283    LiftRefDispatch {
284        /// Environment for evaluating `e2`.
285        env: Env,
286        /// The unevaluated second operand.
287        e2: RcExp,
288    },
289
290    /// LiftRef with non-Code `e1`: `e2` just evaluated. Persist the value
291    /// across stages as `(code (proc v))` — base.scm:151's lift-ref is
292    /// thunk-based value persistence (`(code (proc ,e1 ,(lambda (ignore)
293    /// (e2))))`), never a structural lift. Value identity is preserved:
294    /// closures are not η-recompiled, runtime pairs residualize fine.
295    LiftRefPersist,
296
297    // ── Class O — `Run` (two children, b first) ────────────────────────────────
298
299    /// We evaluated `b`. If Code, push reify scope to compile e and reflect Run.
300    /// Else, enter run-now: snapshot scope, reset, reify-then-reifyv.
301    RunDispatch {
302        /// Environment for evaluating `e`.
303        env: Env,
304        /// The unevaluated expression to run.
305        e: RcExp,
306    },
307
308    // ── Class O — `Trans`/`Evalms` (two children, walk env list afterwards) ────
309
310    /// We evaluated `e_exp`. Now eval `env_exp`, then walk the env list
311    /// and call `staging::trans` + `val_cons_to_exp`.
312    TransRight {
313        /// Environment for evaluating `env_exp`.
314        env: Env,
315        /// The unevaluated name-environment argument.
316        env_exp: RcExp,
317    },
318
319    /// Both args evaluated; walk env_val into a NameEnv list and call trans.
320    TransFinish {
321        /// The already-evaluated expression argument.
322        e_val: Val,
323    },
324
325    /// We evaluated `env_list_exp`. Now eval `expr_exp`, then walk env list
326    /// and recurse into `evalmsg` with explicit env.
327    EvalmsRight {
328        /// Environment for evaluating `expr_exp`.
329        env: Env,
330        /// The unevaluated expression argument.
331        expr_exp: RcExp,
332    },
333
334    /// Both args evaluated; walk env_val into Env, then evalmsg the expr.
335    EvalmsFinish {
336        /// The already-evaluated environment-list argument.
337        env_val: Val,
338    },
339
340    // ── Class S — force-code-then-resume ───────────────────────────────────────
341    //
342    // ForceCode is the iterative form of two *distinct* reference
343    // operations, discriminated by `mode` (see `ForceMode`):
344    //
345    //   - `ForceMode::Strict` — base.scm's `force-code`: Code(e) → e,
346    //     anything else errors "expected code". Used everywhere the
347    //     reference calls `reifyc` or `force-code` directly.
348    //
349    //   - `ForceMode::Lift` — base.scm's `lift` coercion:
350    //       - Code(e) → produce Exp inline, dispatch resume
351    //       - Cst/Sym/Nil/Tup → simple Exp, dispatch resume
352    //       - Clo → enter the lift-Clo η-expansion dance via the kont,
353    //               eventually producing Code(Var(fun_level)) and
354    //               dispatching the same resume.
355    //
356    // The `resume` describes what surrounding construction is waiting
357    // for the Exp.
358
359    /// Force the resumed Val to an Exp per `mode`, then dispatch `resume`.
360    ForceCode {
361        /// Strict (`force-code`) or lenient (`lift`) coercion.
362        mode: ForceMode,
363        /// What to assemble from the forced Exp.
364        resume: ForceCodeResume,
365    },
366
367    // ── Class S — reify-scope exits ────────────────────────────────────────────
368
369    /// Resume from a `reifyv`-style scope: the resumed `Val` is folded
370    /// with the accumulated block. If block is empty, return the val
371    /// as-is; otherwise wrap the val (force_code'd via the kont) into
372    /// a `Val::Code(Let-chain)`. Restore snapshot.
373    ReifyVExit {
374        /// Staging state to restore on exit.
375        saved: ScopeSnapshot,
376        /// What surrounding staged Exp the result slots into.
377        resume: ReifyResume,
378    },
379
380    // ── Class S — the run-now bookkeeping ──────────────────────────────────────
381
382    /// We're inside a `run_now`: the `level` counter has been bumped
383    /// and we are about to do the inner reifyc → outer reifyv dance.
384    /// `RunNowExit` fires when both phases complete and restores `level`.
385    RunNowExit {
386        /// The tower level to restore when the run completes.
387        saved_level: usize,
388    },
389
390    // ── Class S — `lift(Clo)` η-expansion ──────────────────────────────────────
391
392    /// After the body of a lifted Clo finishes evaluation and has been
393    /// force_code'd to an Exp inside the inner reify scope, wrap that
394    /// Exp in `Lam(...)`, reflect into the surrounding block. The
395    /// resulting `Code(Var(fun_level))` is then dispatched per `resume`.
396    LiftCloFinish {
397        /// The fresh variable the memoised Lam was reflected as.
398        fun_level: usize,
399        /// What to assemble from the resulting `Code(Var(fun_level))`.
400        resume: ForceCodeResume,
401    },
402
403    // ── Class C — error-handling boundary ──────────────────────────────────────
404
405    /// `Catch(e)`: marks an error-handling boundary on the kont. Fires
406    /// in two modes:
407    ///   - Success: `step_apply` runs this arm with the body's val and
408    ///     packages it as `(ok val ())`. Saved snapshot is restored,
409    ///     discarding any emitted block bindings (consistent with
410    ///     `run_scope`'s discard-on-restore semantics for Catch).
411    ///   - Error: the machine's unwind walks the kont, restores each
412    ///     popped state-carrying cont, stops at `CatchExit`, restores
413    ///     its snapshot, and packages the error message as
414    ///     `(error msg ())`.
415    CatchExit {
416        /// Staging state to restore on either success or unwind.
417        saved: ScopeSnapshot,
418    },
419
420    // ── Class O — finishing forms with no further dispatch ─────────────────────
421
422    /// `Read(prompt_exp)`: after evaluating the prompt arg, format it,
423    /// call into IO, and parse the resulting string.
424    ReadFinish,
425
426    /// `ReadFile(path_exp)`: after evaluating the path arg (must be a
427    /// `Sym`), read the file, parse all forms, sug each, return them as a
428    /// list of data.
429    ReadFileFinish,
430
431    /// `Throw(payload_exp)`: after evaluating the payload, raise
432    /// `NarjuError::Throw(payload)` — the run loop routes it through
433    /// `unwind_to_catch`, which delivers the payload structurally to the
434    /// nearest `CatchExit`.
435    ThrowFinish,
436}
437
438/// How `Cont::ForceCode` treats a non-`Code` value.
439///
440/// The reference (base.scm) has two distinct operations that narju
441/// once conflated into a single lenient force-code:
442///
443///   - `force-code` — strict projection: `(code e) → e`, anything else
444///     errors `expected code, not ...`. This is what `reifyc` wraps, so
445///     it governs If-Code branches, both Run arms (staged and run-now),
446///     the body of a lift-Clo η-expansion, and `code?`'s staged second
447///     argument. Verified against base.scm running under Chez: e.g.
448///     `(run 0 (lambda (var 1)))`, `(run 0 5)`, `(if (lift 1) 2 3)`,
449///     `(code? (lift 1) 2)`, and `(lift (lambda 5))` all error.
450///
451///   - `lift` — the NBE reify operator: numbers/symbols self-represent,
452///     closures η-expand, pairs recurse on force-code'd components.
453///     This is the *only* lenient coercion in the reference, and only
454///     `lift` itself (plus narju's LiftRef and staged-log paths, whose
455///     strictness base.scm never exercises, so they have no oracle to
456///     check against) may use it.
457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
458pub enum ForceMode {
459    /// `lift` semantics: coerce leniently, η-expand Clo.
460    Lift,
461    /// `force-code` semantics: Code or error.
462    Strict,
463}
464
465/// Discriminator for what to do with the Exp produced by a `ForceCode`
466/// continuation. Different callers want to assemble different surrounding
467/// constructions from the forced Exp.
468pub enum ForceCodeResume {
469    /// We are completing a `reifyc`-style scope: drain `ctx.block` into
470    /// a Let-chain ending in this Exp, restore the snapshot, dispatch
471    /// the original `ReifyResume`.
472    ReifyDrain {
473        /// Staging state to restore after draining the block.
474        saved: ScopeSnapshot,
475        /// What surrounding staged Exp the Let-chain slots into.
476        then: ReifyResume,
477    },
478
479    /// Wrap as `Val::Code(exp)` and produce `Mode::Apply`. Used by
480    /// LiftRef's non-Code-v1 branch.
481    WrapAsCode,
482
483    /// Reflect `Exp::IsCode(s1, exp)` and produce `Mode::Apply`. Used by
484    /// `code?`'s Code-first-arg branch — the paper lifts a non-Code v2
485    /// via `force-code` before constructing the staged check, so this
486    /// resume runs after force-coding v2 (which may have driven a
487    /// lift-Clo η-expansion through the kont stack).
488    IsCodeStaged {
489        /// The first operand's code, already forced.
490        s1: Rc<Exp>,
491    },
492}
493
494/// Discriminator for what shape of staged Exp to assemble when a reify
495/// scope completes. Each reify scope in the recursive code corresponds
496/// to one of these — the resume action knows what surrounding Exp the
497/// reified body slots into.
498pub enum ReifyResume {
499    /// `Exp::If(c, t, f)` with c=Code: t was being reified; f still
500    /// needs reifying. Push a fresh `ReifyCExit { resume: IfFalseDone }`
501    /// scope when this fires.
502    IfTrueDone {
503        /// The condition's code, already forced.
504        c: RcExp,
505        /// Environment for reifying the false branch.
506        env: Env,
507        /// The unevaluated false branch.
508        f: RcExp,
509    },
510
511    /// Both branches reified; reflect the staged If(c, t, f).
512    IfFalseDone {
513        /// The condition's code, already forced.
514        c: RcExp,
515        /// The reified true branch.
516        t: RcExp,
517    },
518
519    /// `Exp::Run(b, e)` with b=Code: reify e, reflect Run(b, e').
520    RunStaged {
521        /// The first operand's code, already forced.
522        b: RcExp,
523    },
524
525    /// `Exp::Run(b, e)` run-now path: inner reifyc completed, now do
526    /// outer reifyv.
527    RunNowInner {
528        /// Environment for the outer reifyv evaluation.
529        env: Env,
530    },
531
532    /// `Exp::LiftRef(e1, e2)` with e1=Code: reify e2, reflect LiftRef.
533    LiftRefDone {
534        /// The first operand's code, already forced.
535        s1: RcExp,
536    },
537
538    /// Resume from `evalmsg`-style top-level reifyv. The result is
539    /// returned directly (no surrounding staged Exp).
540    TopLevel,
541}
542
543// ── Machine ───────────────────────────────────────────────────────────────────
544
545/// The CK machine's execution state.
546///
547/// Owns the `Mode` (current eval/apply step) and the kont stack.
548/// `EvalCtx` is passed alongside via reference because it represents
549/// long-lived staging context (`fresh`, `block`, `fun`, `level`, `io`)
550/// rather than per-evaluation loop state.
551///
552/// A Machine is constructed at the top of an `evalms` call, run to
553/// completion (or error), and then consumed. `Machine::run` drives the
554/// loop and returns the final `Val`.
555pub struct Machine {
556    /// The current step: evaluate an expression or apply a value to the
557    /// kont.
558    pub mode: Mode,
559    /// The continuation stack; empty means the next `Apply` is final.
560    pub kont: Vec<Cont>,
561}
562
563impl Machine {
564    /// Create a fresh machine that will start by evaluating `exp` under
565    /// `env`. The kont stack is empty; the loop terminates when an
566    /// `Apply` finds the stack empty.
567    pub fn new(env: Env, exp: RcExp) -> Self {
568        Self {
569            mode: Mode::Eval { env, exp },
570            kont: Vec::new(),
571        }
572    }
573
574    /// Drive the machine to completion.
575    ///
576    /// Every Exp variant is handled natively; there is no recursive
577    /// baseline. The loop alternates `Mode::Eval` → `step_eval` and
578    /// `Mode::Apply` → pop-cont → `step_apply`, with `unwind_to_catch`
579    /// handling `Catch` error boundaries.
580    pub fn run(self, ctx: &mut EvalCtx) -> Result<Val, crate::error::NarjuError> {
581        // Destructure into locals so the loop can borrow `kont` mutably
582        // while moving `mode` out by value on each iteration.
583        let Self { mut mode, mut kont } = self;
584        loop {
585            let step_result = match mode {
586                Mode::Eval { env, exp } => step_eval(env, exp, &mut kont, ctx),
587                Mode::Apply { val } => match kont.pop() {
588                    None => return Ok(val),
589                    Some(k) => step_apply(k, val, &mut kont, ctx),
590                },
591            };
592            mode = match step_result {
593                Ok(m) => m,
594                Err(e) => unwind_to_catch(&mut kont, e, ctx)?,
595            };
596        }
597    }
598}
599
600// ── step_eval ────────────────────────────────────────────────────────────────
601
602/// Dispatch on the current `Exp` and produce the next `Mode`.
603///
604/// Every Exp variant has an explicit arm; the dispatch is exhaustive.
605/// Two flavours:
606///
607///   1. **Class T** — terminals, `Var`, `Lam`, plus the tail-step
608///      continuations of `Let`/`If`/`App`. Produces `Mode::Apply`
609///      (terminals) or `Mode::Eval` (tail steps).
610///
611///   2. **Class O / S** — operand-bearing forms push a `Cont` onto the
612///      kont and switch to `Mode::Eval` for the first sub-expression.
613///      Resumption goes through `step_apply`. This covers all binary
614///      and unary forms, as well as the staging forms (`If`-Code,
615///      `LiftRef`, `Run`, `Evalms`, lift-`Clo` η-expansion via
616///      `Cont::ForceCode`).
617fn step_eval(
618    env: Env,
619    exp: RcExp,
620    kont: &mut Vec<Cont>,
621    ctx: &mut EvalCtx,
622) -> Result<Mode, crate::error::NarjuError> {
623    use crate::core::Exp;
624    use crate::error::NarjuError;
625
626    match exp.as_ref() {
627        // ── Class T: pure terminals ─────────────────────────────────────────
628        Exp::Nil => Ok(Mode::Apply { val: Val::Nil }),
629        Exp::Lit(n) => Ok(Mode::Apply { val: Val::Cst(*n) }),
630        Exp::Sym(s) => Ok(Mode::Apply { val: Val::Sym(s.clone()) }),
631
632        // Cross-stage persisted value: evaluates to the embedded Val,
633        // mirroring base.scm's `(((tagged? 'proc) e) ((caddr e) env))`.
634        Exp::Proc(v) => Ok(Mode::Apply { val: (**v).clone() }),
635
636        Exp::Var(n) => {
637            let val = env.get(*n).cloned().ok_or_else(|| NarjuError::UnboundVar {
638                level: *n,
639                env_len: env.len(),
640            })?;
641            Ok(Mode::Apply { val })
642        }
643
644        // (lambda e) → (clo env e) — O(1) Rc clones.
645        // Closures from the same Lam node share the Rc<Exp> so find_fun
646        // can ptr_eq during η-expansion.
647        Exp::Lam(body) => Ok(Mode::Apply {
648            val: Val::Clo(Rc::clone(&env), Rc::clone(body)),
649        }),
650
651        // ── Class O — operand-eval prefixed control flow ────────────────────
652        //
653        // Let, If, and App all have an operand-evaluation prefix; control
654        // flow happens in step_apply when that operand finishes evaluating.
655
656        // (let e1 e2): eval e1 onto the kont; on resume, push v1 to env
657        // and tail-step into e2.
658        Exp::Let(e1, e2) => {
659            kont.push(Cont::LetBody {
660                env: Rc::clone(&env),
661                e2: e2.clone(),
662            });
663            Ok(Mode::Eval { env, exp: e1.clone() })
664        }
665
666        // (if c t f): eval c onto the kont, dispatch in step_apply.
667        //   - Cst cond → tail-eval chosen branch
668        //   - Code cond → reify both branches via the kont, reflect new If
669        //   - else → TypeError
670        Exp::If(c, t, f) => {
671            kont.push(Cont::IfDispatch {
672                env: Rc::clone(&env),
673                t: t.clone(),
674                f: f.clone(),
675            });
676            Ok(Mode::Eval { env, exp: c.clone() })
677        }
678
679        // (e1 e2): eval e1 onto kont, dispatch in step_apply.
680        //   - Clo + any → tail-eval body in extended env (Mode::Eval)
681        //   - Code + Code → reflect new App (Mode::Apply)
682        //   - else → TypeError
683        // App's BinOpFinish is special-cased in step_apply because it
684        // can produce Mode::Eval (the Clo tail-step), unlike pure-Val
685        // binops that always produce Mode::Apply.
686        Exp::App(e1, e2) => {
687            kont.push(Cont::BinOpRight {
688                env: Rc::clone(&env),
689                op: BinOpKind::App,
690                e2: e2.clone(),
691            });
692            Ok(Mode::Eval { env, exp: e1.clone() })
693        }
694
695        // ── Class O: binary forms — push BinOpRight, eval e1 ────────────────
696        //
697        // Each pushes a `BinOpRight { env, op, e2 }` onto the kont and
698        // switches to `Mode::Eval` for the first operand. Resumption
699        // sequences via step_apply: `BinOpRight` pushes `BinOpFinish`
700        // and evaluates e2; `BinOpFinish` dispatches on (v1, v2) per
701        // BinOpKind.
702
703        Exp::Plus(e1, e2) => {
704            kont.push(Cont::BinOpRight {
705                env: Rc::clone(&env),
706                op: BinOpKind::Plus,
707                e2: e2.clone(),
708            });
709            Ok(Mode::Eval { env, exp: e1.clone() })
710        }
711        Exp::Minus(e1, e2) => {
712            kont.push(Cont::BinOpRight {
713                env: Rc::clone(&env),
714                op: BinOpKind::Minus,
715                e2: e2.clone(),
716            });
717            Ok(Mode::Eval { env, exp: e1.clone() })
718        }
719        Exp::Times(e1, e2) => {
720            kont.push(Cont::BinOpRight {
721                env: Rc::clone(&env),
722                op: BinOpKind::Times,
723                e2: e2.clone(),
724            });
725            Ok(Mode::Eval { env, exp: e1.clone() })
726        }
727        Exp::Eq(e1, e2) => {
728            kont.push(Cont::BinOpRight {
729                env: Rc::clone(&env),
730                op: BinOpKind::Eq,
731                e2: e2.clone(),
732            });
733            Ok(Mode::Eval { env, exp: e1.clone() })
734        }
735        Exp::Cons(e1, e2) => {
736            kont.push(Cont::BinOpRight {
737                env: Rc::clone(&env),
738                op: BinOpKind::Cons,
739                e2: e2.clone(),
740            });
741            Ok(Mode::Eval { env, exp: e1.clone() })
742        }
743        Exp::IsCode(e1, e2) => {
744            kont.push(Cont::BinOpRight {
745                env: Rc::clone(&env),
746                op: BinOpKind::IsCode,
747                e2: e2.clone(),
748            });
749            Ok(Mode::Eval { env, exp: e1.clone() })
750        }
751        Exp::Log(b_exp, v_exp) => {
752            kont.push(Cont::BinOpRight {
753                env: Rc::clone(&env),
754                op: BinOpKind::Log,
755                e2: v_exp.clone(),
756            });
757            Ok(Mode::Eval { env, exp: b_exp.clone() })
758        }
759
760        // ── Class O: LiftRef — custom dispatch shape ────────────────────────
761        //
762        // LiftRef's `e2` evaluation depends on `v1`: staged via reifyc if
763        // v1 is Code, or normal eval+force_code otherwise. So we can't
764        // use the standard binop sequencing; instead we push a dedicated
765        // `LiftRefDispatch` cont that decides on resume.
766        Exp::LiftRef(e1, e2) => {
767            kont.push(Cont::LiftRefDispatch {
768                env: Rc::clone(&env),
769                e2: e2.clone(),
770            });
771            Ok(Mode::Eval { env, exp: e1.clone() })
772        }
773
774        // ── Class O: unary forms — push UnOpFinish, eval e1 ─────────────────
775
776        Exp::IsNum(e1) => {
777            kont.push(Cont::UnOpFinish { op: UnOpKind::IsNum });
778            Ok(Mode::Eval { env, exp: e1.clone() })
779        }
780        Exp::IsSym(e1) => {
781            kont.push(Cont::UnOpFinish { op: UnOpKind::IsSym });
782            Ok(Mode::Eval { env, exp: e1.clone() })
783        }
784        Exp::IsNil(e1) => {
785            kont.push(Cont::UnOpFinish { op: UnOpKind::IsNil });
786            Ok(Mode::Eval { env, exp: e1.clone() })
787        }
788        Exp::IsPair(e1) => {
789            kont.push(Cont::UnOpFinish { op: UnOpKind::IsPair });
790            Ok(Mode::Eval { env, exp: e1.clone() })
791        }
792        Exp::CellNew(e1) => {
793            kont.push(Cont::UnOpFinish { op: UnOpKind::CellNew });
794            Ok(Mode::Eval { env, exp: e1.clone() })
795        }
796        Exp::CellRead(e1) => {
797            kont.push(Cont::UnOpFinish { op: UnOpKind::CellRead });
798            Ok(Mode::Eval { env, exp: e1.clone() })
799        }
800        Exp::CellSet(e1, e2) => {
801            kont.push(Cont::BinOpRight {
802                env: Rc::clone(&env),
803                op: BinOpKind::CellSet,
804                e2: e2.clone(),
805            });
806            Ok(Mode::Eval { env, exp: e1.clone() })
807        }
808        Exp::Car(e1) => {
809            kont.push(Cont::UnOpFinish { op: UnOpKind::Car });
810            Ok(Mode::Eval { env, exp: e1.clone() })
811        }
812        Exp::Cdr(e1) => {
813            kont.push(Cont::UnOpFinish { op: UnOpKind::Cdr });
814            Ok(Mode::Eval { env, exp: e1.clone() })
815        }
816        Exp::Lift(e1) => {
817            kont.push(Cont::UnOpFinish { op: UnOpKind::Lift });
818            Ok(Mode::Eval { env, exp: e1.clone() })
819        }
820        Exp::Print(e1) => {
821            kont.push(Cont::UnOpFinish { op: UnOpKind::Print });
822            Ok(Mode::Eval { env, exp: e1.clone() })
823        }
824
825        // ── Class O: I/O and staging meta-primitives (batch 3) ──────────────
826
827        // (read prompt-exp): unary — eval prompt, then format + IO + parse.
828        Exp::Read(prompt_exp) => {
829            kont.push(Cont::ReadFinish);
830            Ok(Mode::Eval { env, exp: prompt_exp.clone() })
831        }
832
833        // (read-file path-exp): unary — eval path, then fs + parse + sug.
834        Exp::ReadFile(path_exp) => {
835            kont.push(Cont::ReadFileFinish);
836            Ok(Mode::Eval { env, exp: path_exp.clone() })
837        }
838
839        // (trans e env): binary — eval e, eval env, walk env-list, trans.
840        // Both branches of the eventual dispatch are pure-Val (no Class S
841        // sub-evaluation), so the standard binary sequencing fits.
842        Exp::Trans(e_exp, env_exp) => {
843            kont.push(Cont::TransRight {
844                env: Rc::clone(&env),
845                env_exp: env_exp.clone(),
846            });
847            Ok(Mode::Eval { env, exp: e_exp.clone() })
848        }
849
850        // (evalms env-list expr): binary — eval env-list, eval expr, walk
851        // env-list into Env, then enter a reifyv scope to evaluate the expr
852        // under that explicit env.
853        Exp::Evalms(env_list_exp, expr_exp) => {
854            kont.push(Cont::EvalmsRight {
855                env: Rc::clone(&env),
856                expr_exp: expr_exp.clone(),
857            });
858            Ok(Mode::Eval { env, exp: env_list_exp.clone() })
859        }
860
861        // (run b e): unary in b — eval b, dispatch on v1 in step_apply.
862        // Code-b → reify e; non-Code-b → run-now (save level, inner reifyc
863        // then outer reifyv via the kont).
864        Exp::Run(b, e) => {
865            kont.push(Cont::RunDispatch {
866                env: Rc::clone(&env),
867                e: e.clone(),
868            });
869            Ok(Mode::Eval { env, exp: b.clone() })
870        }
871
872        // (catch e): save scope, push CatchExit, eval body. Success
873        // produces (ok val ()); error during body is caught by the loop's
874        // unwind and produces (error msg ()).
875        Exp::Catch(e) => {
876            let saved = ScopeSnapshot::save_and_reset_block(ctx);
877            kont.push(Cont::CatchExit { saved });
878            Ok(Mode::Eval { env, exp: e.clone() })
879        }
880
881        // (throw v): unary — eval payload, then raise it via the unwind.
882        Exp::Throw(e) => {
883            kont.push(Cont::ThrowFinish);
884            Ok(Mode::Eval { env, exp: e.clone() })
885        }
886    }
887}
888
889// ── step_apply ───────────────────────────────────────────────────────────────
890
891/// Resume a paused operation by feeding the just-produced `val` to the
892/// popped continuation `k`.
893///
894/// Each Cont variant has an explicit arm. The remaining `_` is for the
895/// narrower destructured patterns (`BinOpFinish { op: App, .. }`,
896/// `BinOpFinish { op: Log, v1: Code, .. }`, `UnOpFinish { op: Lift }`)
897/// where the variant is matched more than once with progressively wider
898/// patterns.
899fn step_apply(
900    k: Cont,
901    val: Val,
902    kont: &mut Vec<Cont>,
903    ctx: &mut EvalCtx,
904) -> Result<Mode, crate::error::NarjuError> {
905    use crate::core::{code, env_push, env_push_owned, rc_exp, Exp};
906    use crate::error::NarjuError;
907
908    match k {
909        // Resumed with v1; need to evaluate e2 next.
910        // Push BinOpFinish to remember v1 and the op, then switch to Eval.
911        Cont::BinOpRight { env, op, e2 } => {
912            kont.push(Cont::BinOpFinish { op, v1: val });
913            Ok(Mode::Eval { env, exp: e2 })
914        }
915
916        // Both operands evaluated. v1 was saved on the cont; v2 is the
917        // resumed Val.
918        //
919        // App is special-cased here because its Clo dispatch produces
920        // `Mode::Eval` (a tail jump into the closure body), which
921        // doesn't fit the `Result<Val, _>` shape of `dispatch_binop`.
922        // Other binops produce a Val and switch to Mode::Apply.
923        Cont::BinOpFinish { op: BinOpKind::App, v1 } => {
924            let v2 = val;
925            match (v1, v2) {
926                (Val::Code(s1), Val::Code(s2)) => {
927                    let result = ctx.reflectc(Exp::App(s1, s2));
928                    Ok(Mode::Apply { val: result })
929                }
930                (Val::Clo(clo_env, body), v2) => {
931                    let self_val = Val::Clo(Rc::clone(&clo_env), Rc::clone(&body));
932                    // Inner push: clo_env is shared with the source closure,
933                    // so make_mut clones the Vec. Outer push: the inner result
934                    // has refcount 1, so env_push_owned mutates it in place.
935                    let new_env = env_push_owned(env_push(&clo_env, self_val), v2);
936                    Ok(Mode::Eval {
937                        env: new_env,
938                        exp: Rc::clone(&body),
939                    })
940                }
941                (f, a) => Err(NarjuError::TypeError {
942                    op: "app",
943                    expected: "Clo or Code",
944                    actual: format!("({}, {})", f.short_desc(), a.short_desc()),
945                }),
946            }
947        }
948
949        // Log's staged branch needs to force_code v2, which can drive
950        // a lift-Clo η-expansion via the kont. Special-cased here so
951        // we can push a ForceCode cont rather than calling force_code
952        // inline (which would recurse through Rust on a Clo argument).
953        // Staged log (v1 = Code): the reference reflects
954        //   (log ,(force-code v1)
955        //        ,(force-code (if (code? v2) v2 (lift-ref '_ (lambda () v2)))))
956        // — a non-code v2 is *persisted across stages* as a proc exp
957        // (`Exp::Proc`), not coerced to syntax. This preserves value
958        // identity (closures are not η-recompiled; pairs of run-time
959        // values residualize fine). Both cases produce an Exp
960        // synchronously, so no ForceCode continuation is needed.
961        Cont::BinOpFinish { op: BinOpKind::Log, v1: Val::Code(sb) } => {
962            let v2_exp = match val {
963                Val::Code(e) => Rc::unwrap_or_clone(e),
964                v2 => Exp::Proc(crate::core::rc_val(v2)),
965            };
966            let result = ctx.reflectc(Exp::Log(sb, rc_exp(v2_exp)));
967            Ok(Mode::Apply { val: result })
968        }
969
970        // `code?`'s staged branch (v1 = Code): the reference reflects
971        // `(code? ,(force-code v1) ,(force-code v2))` — *strict* on v2.
972        // A non-Code v2 under a staged first argument is an error, not
973        // a lift. (An earlier narju revision lifted v2 here, misreading
974        // base.scm's force-code as lenient; it never lifts.)
975        Cont::BinOpFinish { op: BinOpKind::IsCode, v1: Val::Code(s1) } => {
976            kont.push(Cont::ForceCode {
977                mode: ForceMode::Strict,
978                resume: ForceCodeResume::IsCodeStaged { s1 },
979            });
980            Ok(Mode::Apply { val })
981        }
982
983        Cont::BinOpFinish { op, v1 } => {
984            let result = dispatch_binop(op, v1, val, ctx)?;
985            Ok(Mode::Apply { val: result })
986        }
987
988        // Lift's Clo case may drive lift-Clo η-expansion via the kont.
989        // For all other Val variants the lift is O(1) and goes through
990        // dispatch_unop. For Clo, we push ForceCode-with-WrapAsCode and
991        // re-dispatch the val; ForceCode handles Clo by entering the
992        // lift-Clo dance with the same WrapAsCode resume.
993        Cont::UnOpFinish { op: UnOpKind::Lift } => {
994            match val {
995                Val::Clo(_, _) => {
996                    kont.push(Cont::ForceCode {
997                        mode: ForceMode::Lift,
998                        resume: ForceCodeResume::WrapAsCode,
999                    });
1000                    Ok(Mode::Apply { val })
1001                }
1002                _ => {
1003                    let result = dispatch_unop(UnOpKind::Lift, val, ctx)?;
1004                    Ok(Mode::Apply { val: result })
1005                }
1006            }
1007        }
1008
1009        // Single operand evaluated. Dispatch per UnOpKind.
1010        Cont::UnOpFinish { op } => {
1011            let result = dispatch_unop(op, val, ctx)?;
1012            Ok(Mode::Apply { val: result })
1013        }
1014
1015        // (let e1 e2) with e1 just evaluated to `val`. Push val to env
1016        // and tail-step into e2. The `env` here was moved out of the
1017        // `Cont::LetBody`, so we own it — `env_push_owned` mutates the
1018        // Vec in place when refcount is 1.
1019        Cont::LetBody { env, e2 } => {
1020            let new_env = env_push_owned(env, val);
1021            Ok(Mode::Eval { env: new_env, exp: e2 })
1022        }
1023
1024        // (if c t f) with c just evaluated to `val`.
1025        //   - Cst(n)  → tail-step into chosen branch (Mode::Eval)
1026        //   - Code(s) → enter a reify scope for the t-branch, with a
1027        //               chained resume that does the f-branch next.
1028        //               After both branches reify, reflect new If.
1029        //   - else    → TypeError
1030        Cont::IfDispatch { env, t, f } => {
1031            match val {
1032                Val::Cst(n) => {
1033                    let chosen = if n != 0 { t } else { f };
1034                    Ok(Mode::Eval { env, exp: chosen })
1035                }
1036                Val::Code(vc) => {
1037                    // Enter the t-branch reify scope. On body completion,
1038                    // ForceCode forces it to an Exp; ReifyDrain then drains
1039                    // the block, restores snapshot, and dispatches
1040                    // IfTrueDone to set up the f-branch reify scope.
1041                    // Strict: the reference wraps each branch in `reifyc`,
1042                    // so a branch evaluating to non-Code is an error.
1043                    let saved = ScopeSnapshot::save_and_reset_block(ctx);
1044                    kont.push(Cont::ForceCode {
1045                        mode: ForceMode::Strict,
1046                        resume: ForceCodeResume::ReifyDrain {
1047                            saved,
1048                            then: ReifyResume::IfTrueDone {
1049                                c: vc,
1050                                env: Rc::clone(&env),
1051                                f,
1052                            },
1053                        },
1054                    });
1055                    Ok(Mode::Eval { env, exp: t })
1056                }
1057                other => Err(NarjuError::TypeError {
1058                    op: "if",
1059                    expected: "Cst or Code",
1060                    actual: other.short_desc(),
1061                }),
1062            }
1063        }
1064
1065        // LiftRef e1 e2 with e1 just evaluated to `val`.
1066        //   - Code(s1) → enter reify scope for e2; on completion
1067        //                ForceCode/ReifyDrain dispatches LiftRefDone
1068        //                which reflects the staged LiftRef.
1069        //   - else     → eval e2 normally; on completion LiftRefPersist
1070        //                wraps the value as `(code (proc v))` — base.scm's
1071        //                lift-ref is thunk-based cross-stage persistence
1072        //                (never a structural lift), so free closures and
1073        //                runtime pairs keep their identity. Eager where
1074        //                the reference thunks, but pink dispatches
1075        //                lift-ref with both args pre-evaluated
1076        //                (pink.scm:52), so laziness is unobservable there.
1077        Cont::LiftRefDispatch { env, e2 } => {
1078            match val {
1079                Val::Code(s1) => {
1080                    let saved = ScopeSnapshot::save_and_reset_block(ctx);
1081                    kont.push(Cont::ForceCode {
1082                        mode: ForceMode::Lift,
1083                        resume: ForceCodeResume::ReifyDrain {
1084                            saved,
1085                            then: ReifyResume::LiftRefDone { s1 },
1086                        },
1087                    });
1088                    Ok(Mode::Eval { env, exp: e2 })
1089                }
1090                _ => {
1091                    kont.push(Cont::LiftRefPersist);
1092                    Ok(Mode::Eval { env, exp: e2 })
1093                }
1094            }
1095        }
1096
1097        Cont::LiftRefPersist => {
1098            let result = Val::Code(rc_exp(Exp::Proc(crate::core::rc_val(val))));
1099            Ok(Mode::Apply { val: result })
1100        }
1101
1102        // (read prompt) with prompt evaluated to `val`. Format the prompt
1103        // string from the Val, call `ctx.io.read_line`, parse the result.
1104        // Eof / blank line / interrupt → Nil. Parse errors propagate.
1105        Cont::ReadFinish => {
1106            use super::val_to_prompt;
1107            use crate::parse::{parse_one, sexp_to_val};
1108            let prompt_str = val_to_prompt(&val);
1109            let result_val = match ctx.io.read_line(&prompt_str)? {
1110                None => Val::Nil,
1111                Some(line) => {
1112                    let line = line.trim().to_string();
1113                    if line.is_empty() {
1114                        Val::Nil
1115                    } else {
1116                        match parse_one(&line) {
1117                            Ok(sexp) => sexp_to_val(&sexp),
1118                            Err(e) => return Err(NarjuError::Parse(format!("read: {e}"))),
1119                        }
1120                    }
1121                }
1122            };
1123            Ok(Mode::Apply { val: result_val })
1124        }
1125
1126        // (read-file path) with path evaluated to `val`. The path must be a
1127        // symbol (`.` and `/` are symbol chars). Each parsed form is sugged
1128        // and qq-expanded — read-file reads *source*, and both are
1129        // read-time source transforms (pink.scm sugs its quoted source at
1130        // definition; Scheme gives the reference its quasiquote) — then
1131        // converted to data. Result: list of forms.
1132        Cont::ReadFileFinish => {
1133            use crate::parse::{expand_qq, parse_all, sexp_to_val, sug};
1134            let path = match &val {
1135                Val::Sym(s) => s.as_str(),
1136                other => {
1137                    return Err(NarjuError::TypeError {
1138                        op: "read-file",
1139                        expected: "symbol path",
1140                        actual: other.short_desc(),
1141                    })
1142                }
1143            };
1144            let src = crate::io::read_source(path)?;
1145            let forms = parse_all(&src)
1146                .map_err(|e| NarjuError::Parse(format!("read-file {path}: {e}")))?;
1147            let result = forms.iter().rev().try_fold(Val::Nil, |acc, form| {
1148                let expanded = expand_qq(&sug(form))
1149                    .map_err(|e| NarjuError::Parse(format!("read-file {path}: {e}")))?;
1150                Ok::<_, NarjuError>(crate::core::tup(sexp_to_val(&expanded), acc))
1151            })?;
1152            Ok(Mode::Apply { val: result })
1153        }
1154
1155        // (throw v) with payload evaluated to `val`: raise. The run loop
1156        // routes the Err through unwind_to_catch, which either delivers
1157        // the payload to a CatchExit or propagates as "uncaught throw".
1158        Cont::ThrowFinish => Err(NarjuError::Throw(val)),
1159
1160        // (trans e env) with e evaluated to `val`. Save it on the cont,
1161        // switch to evaluate env_exp.
1162        Cont::TransRight { env, env_exp } => {
1163            kont.push(Cont::TransFinish { e_val: val });
1164            Ok(Mode::Eval { env, exp: env_exp })
1165        }
1166
1167        // (trans e env) with both args evaluated; `e_val` was saved on the
1168        // cont; the resumed `val` is env_val. Walk env_val into a NameEnv,
1169        // then call staging::trans + val_cons_to_exp.
1170        Cont::TransFinish { e_val } => {
1171            use super::staging::{trans, val_cons_to_exp, NameEntry};
1172            let env_val = val;
1173            let mut name_env: Vec<NameEntry> = Vec::new();
1174            let mut cur = env_val;
1175            loop {
1176                match cur {
1177                    Val::Nil => break,
1178                    Val::Tup(h, t) => {
1179                        let entry = match h.as_ref() {
1180                            Val::Sym(s) => NameEntry::Name(s.clone()),
1181                            Val::Tup(name_val, code_val) => {
1182                                let name = match name_val.as_ref() {
1183                                    Val::Sym(s) => s.clone(),
1184                                    other => return Err(NarjuError::Stage(format!(
1185                                        "trans: splice pair car must be Sym, got {other:?}"
1186                                    ))),
1187                                };
1188                                let exp = match code_val.as_ref() {
1189                                    Val::Code(e) => (**e).clone(),
1190                                    other => return Err(NarjuError::Stage(format!(
1191                                        "trans: splice pair cdr must be Code, got {other:?}"
1192                                    ))),
1193                                };
1194                                NameEntry::Splice { name, exp }
1195                            }
1196                            other => return Err(NarjuError::Stage(format!(
1197                                "trans: env entries must be Sym or (cons name Code), got {other:?}"
1198                            ))),
1199                        };
1200                        name_env.push(entry);
1201                        cur = Rc::unwrap_or_clone(t);
1202                    }
1203                    other => return Err(NarjuError::Stage(format!(
1204                        "trans: env must be a list, got {other:?}"
1205                    ))),
1206                }
1207            }
1208            let transd = trans(&e_val, &name_env)?;
1209            let exp = val_cons_to_exp(&transd)?;
1210            Ok(Mode::Apply { val: code(exp) })
1211        }
1212
1213        // (evalms env-list expr) with env-list evaluated. Save it, eval expr.
1214        Cont::EvalmsRight { env, expr_exp } => {
1215            kont.push(Cont::EvalmsFinish { env_val: val });
1216            Ok(Mode::Eval { env, exp: expr_exp })
1217        }
1218
1219        // (evalms env-list expr) with both evaluated. Walk env-list into Env,
1220        // then enter a reifyv scope to evaluate the expr under that env.
1221        Cont::EvalmsFinish { env_val } => {
1222            use crate::core::{env_new, env_push_owned};
1223            let expr_val = val;
1224            let mut explicit_env = env_new();
1225            let mut cur = env_val;
1226            loop {
1227                match cur {
1228                    Val::Nil => break,
1229                    Val::Tup(h, t) => {
1230                        // explicit_env is a fresh-built local — refcount 1
1231                        // through the entire loop, so env_push_owned mutates
1232                        // the underlying Vec in place each iteration rather
1233                        // than cloning it.
1234                        explicit_env = env_push_owned(explicit_env, Rc::unwrap_or_clone(h));
1235                        cur = Rc::unwrap_or_clone(t);
1236                    }
1237                    other => return Err(NarjuError::Stage(format!(
1238                        "evalms: env must be a list, got {other:?}"
1239                    ))),
1240                }
1241            }
1242            let exp = match expr_val {
1243                Val::Code(exp) => exp,
1244                other => return Err(NarjuError::TypeError {
1245                    op: "evalms",
1246                    expected: "Code",
1247                    actual: other.short_desc(),
1248                }),
1249            };
1250            // evalmsg = reifyv(|ctx| evalms(env, exp, ctx)).
1251            // Enter the reifyv scope and evaluate exp under explicit_env.
1252            let saved = ScopeSnapshot::save_and_reset_block(ctx);
1253            kont.push(Cont::ReifyVExit {
1254                saved,
1255                resume: ReifyResume::TopLevel,
1256            });
1257            Ok(Mode::Eval { env: explicit_env, exp })
1258        }
1259
1260        // (run b e) with b evaluated to `val`.
1261        //   - Code(b1) → enter reify scope for e; on completion
1262        //                ForceCode/ReifyDrain dispatches RunStaged
1263        //                which reflects Run(b, e').
1264        //   - else     → run-now path: save level, push outer reifyv +
1265        //                inner reifyc, eval e under reset fresh.
1266        Cont::RunDispatch { env, e } => {
1267            match val {
1268                Val::Code(b1) => {
1269                    // Strict: reference is `reflectc(run ,b1 ,(reifyc thunk))`.
1270                    let saved = ScopeSnapshot::save_and_reset_block(ctx);
1271                    kont.push(Cont::ForceCode {
1272                        mode: ForceMode::Strict,
1273                        resume: ForceCodeResume::ReifyDrain {
1274                            saved,
1275                            then: ReifyResume::RunStaged { b: b1 },
1276                        },
1277                    });
1278                    Ok(Mode::Eval { env, exp: e })
1279                }
1280                _ => {
1281                    // Run-now: save level, push outer reifyv + inner reifyc,
1282                    // reset fresh = env_len for the inner scope.
1283                    let env_len = env.len();
1284
1285                    // Push level-restore (fires last after both reify scopes).
1286                    kont.push(Cont::RunNowExit {
1287                        saved_level: ctx.level,
1288                    });
1289                    ctx.level += 1;
1290
1291                    // Push outer reifyv (fires after inner reifyc completes
1292                    // and we evaluate the inner-produced let-chain).
1293                    let saved_outer = ScopeSnapshot::save_and_reset_block(ctx);
1294                    kont.push(Cont::ReifyVExit {
1295                        saved: saved_outer,
1296                        resume: ReifyResume::TopLevel,
1297                    });
1298
1299                    // Push inner reifyc (fires first, holding the produced
1300                    // Exp `inner`; resume RunNowInner switches to evaluating
1301                    // inner under env, feeding into the outer reifyv).
1302                    // Strict: `(run 0 <non-code>)` must error "expected
1303                    // code", not silently η-expand (base.scm errors here).
1304                    let saved_inner = ScopeSnapshot::save_and_reset_block(ctx);
1305                    kont.push(Cont::ForceCode {
1306                        mode: ForceMode::Strict,
1307                        resume: ForceCodeResume::ReifyDrain {
1308                            saved: saved_inner,
1309                            then: ReifyResume::RunNowInner {
1310                                env: Rc::clone(&env),
1311                            },
1312                        },
1313                    });
1314
1315                    // Inner reifyc resets fresh per the recursive baseline.
1316                    ctx.fresh = env_len;
1317
1318                    Ok(Mode::Eval { env, exp: e })
1319                }
1320            }
1321        }
1322
1323        // ── Class S — ForceCode and reify-scope exits ──────────────────────
1324        //
1325        // ForceCode is the iterative form of `force_code(v)`. It examines
1326        // the resumed `val`:
1327        //   - Code/Cst/Sym/Nil/Tup → produce Exp inline, dispatch resume.
1328        //   - Clo → enter the lift-Clo η-expansion: register the closure,
1329        //           save a reify scope snapshot, fresh-allocate f/x vars,
1330        //           push LiftCloFinish + ForceCode-with-ReifyDrain, and
1331        //           switch into evaluating the closure body. The eventual
1332        //           Code result flows through LiftCloFinish (which wraps
1333        //           in Lam, reflects, and re-dispatches the same resume).
1334        //
1335        // The `resume` indicates what surrounding construction is waiting
1336        // for the produced Exp.
1337
1338        Cont::ForceCode { mode, resume } => {
1339            // Strict mode is the reference's `force-code`: only Code
1340            // projects; everything else — including Clo — errors. The
1341            // η-expansion arms below are `lift`-only territory.
1342            if let ForceMode::Strict = mode {
1343                return match val {
1344                    Val::Code(e) => {
1345                        force_code_dispatch_resume(resume, (*e).clone(), kont, ctx)
1346                    }
1347                    other => Err(NarjuError::Stage(format!(
1348                        "force-code: expected code, not {}",
1349                        other.short_desc()
1350                    ))),
1351                };
1352            }
1353            match val {
1354                Val::Code(e) => {
1355                    let exp = (*e).clone();
1356                    force_code_dispatch_resume(resume, exp, kont, ctx)
1357                }
1358                Val::Cst(n) => force_code_dispatch_resume(resume, Exp::Lit(n), kont, ctx),
1359                Val::Sym(s) => force_code_dispatch_resume(resume, Exp::Sym(s), kont, ctx),
1360                Val::Nil => force_code_dispatch_resume(resume, Exp::Nil, kont, ctx),
1361                // Staging rule: cells never fold. Even the lenient
1362                // lift coercion refuses to freeze a cell into syntax.
1363                Val::Cell(_) => Err(NarjuError::Stage(
1364                    "cannot lift a cell: cells never fold under staging".into(),
1365                )),
1366                Val::Tup(a, b) => {
1367                    let ca = match &*a {
1368                        Val::Code(e) => (**e).clone(),
1369                        v => return Err(NarjuError::Stage(format!(
1370                            "lift Tup: car must be Code, got {}",
1371                            v.short_desc()
1372                        ))),
1373                    };
1374                    let cb = match &*b {
1375                        Val::Code(e) => (**e).clone(),
1376                        v => return Err(NarjuError::Stage(format!(
1377                            "lift Tup: cdr must be Code, got {}",
1378                            v.short_desc()
1379                        ))),
1380                    };
1381                    let exp = ctx.reflect(Exp::Cons(rc_exp(ca), rc_exp(cb)));
1382                    force_code_dispatch_resume(resume, exp, kont, ctx)
1383                }
1384                Val::Clo(clo_env, body) => {
1385                    // Fast path: this closure was already η-expanded in
1386                    // the current scope. Re-use its de Bruijn level.
1387                    if let Some(n) = ctx.find_fun(&clo_env, &body) {
1388                        return force_code_dispatch_resume(
1389                            resume,
1390                            Exp::Var(n),
1391                            kont,
1392                            ctx,
1393                        );
1394                    }
1395                    let fun_level = ctx.fresh;
1396                    ctx.register_fun(fun_level, &clo_env, &body);
1397
1398                    // Set up the reify scope for the body. Save snapshot
1399                    // (mem::take leaves block empty for the new scope).
1400                    let saved = ScopeSnapshot::save_and_reset_block(ctx);
1401                    // Allocate f and x vars *inside* the scope, after save.
1402                    let f_var = Exp::Var(ctx.fresh_var());
1403                    let x_var = Exp::Var(ctx.fresh_var());
1404                    // Inner push: clo_env is shared with the source closure,
1405                    // so make_mut clones the Vec. Outer push consumes the
1406                    // refcount-1 inner result and mutates it in place.
1407                    let env2 = env_push_owned(
1408                        env_push(&clo_env, code(f_var)),
1409                        code(x_var),
1410                    );
1411
1412                    // Push LiftCloFinish (fires after the reify completes;
1413                    // wraps in Lam, reflects, dispatches resume).
1414                    kont.push(Cont::LiftCloFinish {
1415                        fun_level,
1416                        resume,
1417                    });
1418                    // Push the ForceCode that will fire when the body
1419                    // produces its val; its ReifyDrain resume hands the
1420                    // resulting Exp back to LiftCloFinish via Code.
1421                    // Strict: the reference η-expansion is
1422                    // `(reify (lambda () (force-code (evalms ...))))` —
1423                    // a lifted closure's body must itself produce code
1424                    // (`(lift (lambda 5))` errors; lift the 5 explicitly).
1425                    kont.push(Cont::ForceCode {
1426                        mode: ForceMode::Strict,
1427                        resume: ForceCodeResume::ReifyDrain {
1428                            saved,
1429                            then: ReifyResume::TopLevel,
1430                        },
1431                    });
1432                    Ok(Mode::Eval {
1433                        env: env2,
1434                        exp: Rc::clone(&body),
1435                    })
1436                }
1437            }
1438        }
1439
1440        // After the body of a lifted Clo has been reified to a Code val,
1441        // wrap it in Lam(...) and reflect into the outer block. The
1442        // resulting Code(Var(fun_level)) is then dispatched per resume.
1443        Cont::LiftCloFinish { fun_level: _, resume } => {
1444            let body_exp = match val {
1445                Val::Code(e) => Rc::unwrap_or_clone(e),
1446                v => return Err(NarjuError::Stage(format!(
1447                    "lift Clo: body did not reify to Code, got {}",
1448                    v.short_desc()
1449                ))),
1450            };
1451            // reflect Lam(body) into the outer block; produces Var(fresh)
1452            // which equals fun_level by construction (snapshot restored
1453            // fresh to its pre-scope value, which was fun_level).
1454            let result_exp = ctx.reflect(Exp::Lam(rc_exp(body_exp)));
1455            force_code_dispatch_resume(resume, result_exp, kont, ctx)
1456        }
1457
1458        // ReifyVExit completes a `reifyv`-style scope: drain ctx.block;
1459        // if empty, produce `val` as-is; else val must be Code, extract
1460        // its Exp, wrap in Let-chain, produce Val::Code(let-chain).
1461        // Then restore snapshot and dispatch on resume.
1462        Cont::ReifyVExit { saved, resume } => {
1463            let stmts = std::mem::take(&mut ctx.block);
1464            let result_val = if stmts.is_empty() {
1465                val
1466            } else {
1467                let last = match val {
1468                    Val::Code(e) => Rc::unwrap_or_clone(e),
1469                    v => return Err(NarjuError::Stage(format!(
1470                        "reifyv: non-empty block but non-Code result: {}",
1471                        v.short_desc()
1472                    ))),
1473                };
1474                let exp = stmts
1475                    .into_iter()
1476                    .rev()
1477                    .fold(last, |acc, stmt| Exp::Let(rc_exp(stmt), rc_exp(acc)));
1478                code(exp)
1479            };
1480            saved.restore(ctx);
1481            match resume {
1482                ReifyResume::TopLevel => {
1483                    Ok(Mode::Apply { val: result_val })
1484                }
1485                // No other reify-resume is currently used with reifyv —
1486                // all our reifyv sites (Evalms, Run-now-outer) want
1487                // TopLevel-style "just produce the val."
1488                _ => unreachable!(
1489                    "ReifyVExit reached a resume kind only valid for ReifyCExit"
1490                ),
1491            }
1492        }
1493
1494        // RunNowExit fires after the run-now's outer reifyv completes.
1495        // Restore the saved level, forward `val` unchanged.
1496        Cont::RunNowExit { saved_level } => {
1497            ctx.level = saved_level;
1498            Ok(Mode::Apply { val })
1499        }
1500
1501        // CatchExit success path: body produced `val` without erroring.
1502        // Restore the saved snapshot (discarding any emitted bindings —
1503        // Catch is for unstaged user code; staged effects don't leak)
1504        // and package as `(ok val ())`.
1505        Cont::CatchExit { saved } => {
1506            use crate::core::tup;
1507            saved.restore(ctx);
1508            let result = tup(
1509                Val::Sym("ok".into()),
1510                tup(val, Val::Nil),
1511            );
1512            Ok(Mode::Apply { val: result })
1513        }
1514
1515    }
1516}
1517
1518// ── Class S dispatch helper ──────────────────────────────────────────────────
1519
1520/// Given an Exp `produced` from a force-code-style operation and a
1521/// `ForceCodeResume` describing what to do with it, produce the next Mode.
1522///
1523/// For `ReifyDrain`: drain `ctx.block` into a Let-chain ending in `produced`,
1524/// restore the saved snapshot, then dispatch the original `ReifyResume`
1525/// against the resulting Exp.
1526///
1527/// For `WrapAsCode`: wrap as `Val::Code(produced)` and produce `Mode::Apply`.
1528///
1529/// For `LogStaged { sb }`: reflect `Exp::Log(sb, produced)` and produce
1530/// `Mode::Apply { val: code }`.
1531fn force_code_dispatch_resume(
1532    resume: ForceCodeResume,
1533    produced: Exp,
1534    kont: &mut Vec<Cont>,
1535    ctx: &mut EvalCtx,
1536) -> Result<Mode, crate::error::NarjuError> {
1537    use crate::core::{code, rc_exp};
1538    match resume {
1539        ForceCodeResume::WrapAsCode => {
1540            Ok(Mode::Apply { val: code(produced) })
1541        }
1542        ForceCodeResume::IsCodeStaged { s1 } => {
1543            let val = ctx.reflectc(Exp::IsCode(s1, rc_exp(produced)));
1544            Ok(Mode::Apply { val })
1545        }
1546        ForceCodeResume::ReifyDrain { saved, then } => {
1547            // Drain block into Let-chain ending in `produced`.
1548            let stmts = std::mem::take(&mut ctx.block);
1549            let result_exp = stmts
1550                .into_iter()
1551                .rev()
1552                .fold(produced, |acc, stmt| {
1553                    Exp::Let(rc_exp(stmt), rc_exp(acc))
1554                });
1555            // Restore the reify scope's saved snapshot.
1556            saved.restore(ctx);
1557            // Dispatch the original ReifyResume against result_exp.
1558            reify_dispatch(then, result_exp, kont, ctx)
1559        }
1560    }
1561}
1562
1563/// Given the staged Exp produced by a reify scope and the `ReifyResume`
1564/// describing what surrounding construction is being assembled, produce
1565/// the next Mode.
1566fn reify_dispatch(
1567    resume: ReifyResume,
1568    result_exp: Exp,
1569    kont: &mut Vec<Cont>,
1570    ctx: &mut EvalCtx,
1571) -> Result<Mode, crate::error::NarjuError> {
1572    use crate::core::{code, rc_exp};
1573    match resume {
1574        ReifyResume::IfTrueDone { c, env, f } => {
1575            // result_exp is the staged t-branch. Set up the f-branch
1576            // reify scope; on its completion IfFalseDone reflects If.
1577            // Strict for the same reason as the t-branch: reifyc.
1578            let saved2 = ScopeSnapshot::save_and_reset_block(ctx);
1579            kont.push(Cont::ForceCode {
1580                mode: ForceMode::Strict,
1581                resume: ForceCodeResume::ReifyDrain {
1582                    saved: saved2,
1583                    then: ReifyResume::IfFalseDone {
1584                        c,
1585                        t: rc_exp(result_exp),
1586                    },
1587                },
1588            });
1589            Ok(Mode::Eval { env, exp: f })
1590        }
1591        ReifyResume::IfFalseDone { c, t } => {
1592            let val = ctx.reflectc(Exp::If(c, t, rc_exp(result_exp)));
1593            Ok(Mode::Apply { val })
1594        }
1595        ReifyResume::RunStaged { b } => {
1596            let val = ctx.reflectc(Exp::Run(b, rc_exp(result_exp)));
1597            Ok(Mode::Apply { val })
1598        }
1599        ReifyResume::LiftRefDone { s1 } => {
1600            let val = ctx.reflectc(Exp::LiftRef(s1, rc_exp(result_exp)));
1601            Ok(Mode::Apply { val })
1602        }
1603        ReifyResume::RunNowInner { env } => {
1604            // result_exp is the inner-reifyc-produced Exp. The outer
1605            // ReifyVExit is on the kont stack; switch to evaluating
1606            // result_exp under env so its Val flows up to the outer.
1607            Ok(Mode::Eval { env, exp: rc_exp(result_exp) })
1608        }
1609        ReifyResume::TopLevel => {
1610            // Used by lift-Clo's inner reify (TopLevel hands off the
1611            // body Exp wrapped as Code to the surrounding LiftCloFinish).
1612            Ok(Mode::Apply { val: code(result_exp) })
1613        }
1614    }
1615}
1616
1617// ── Error unwind ─────────────────────────────────────────────────────────────
1618
1619/// Walk the kont stack on error, restoring state of each popped cont,
1620/// until we find a `CatchExit`. If found, restore its snapshot, package
1621/// the error message as `(error msg ())`, and produce `Mode::Apply` for
1622/// the loop to resume normal execution. If we exhaust the kont without
1623/// finding a `CatchExit`, propagate the original error.
1624///
1625/// This is the only place where error handling crosses the cont boundary;
1626/// elsewhere `?` is used normally and produces a fresh `Err` value, which
1627/// the run loop catches and routes here.
1628fn unwind_to_catch(
1629    kont: &mut Vec<Cont>,
1630    err: crate::error::NarjuError,
1631    ctx: &mut EvalCtx,
1632) -> Result<Mode, crate::error::NarjuError> {
1633    use crate::core::tup;
1634    while let Some(k) = kont.pop() {
1635        match k {
1636            Cont::CatchExit { saved } => {
1637                saved.restore(ctx);
1638                // A thrown payload arrives structurally; machine errors
1639                // arrive as a Sym of their message. Both are
1640                // `(error <payload> ())`, so catch consumers stay uniform.
1641                let payload = match err {
1642                    crate::error::NarjuError::Throw(v) => v,
1643                    other => Val::Sym(other.to_string().into()),
1644                };
1645                let val = tup(
1646                    Val::Sym("error".into()),
1647                    tup(payload, Val::Nil),
1648                );
1649                return Ok(Mode::Apply { val });
1650            }
1651            // Conts carrying restorable state: restore before discarding.
1652            Cont::ForceCode {
1653                resume: ForceCodeResume::ReifyDrain { saved, .. },
1654                ..
1655            }
1656            | Cont::LiftCloFinish {
1657                resume: ForceCodeResume::ReifyDrain { saved, .. },
1658                ..
1659            }
1660            | Cont::ReifyVExit { saved, .. } => {
1661                saved.restore(ctx);
1662            }
1663            Cont::RunNowExit { saved_level } => {
1664                ctx.level = saved_level;
1665            }
1666            // All other conts hold no ctx state — just drop.
1667            _ => {}
1668        }
1669    }
1670    Err(err)
1671}
1672
1673// ── Class O dispatch helpers ─────────────────────────────────────────────────
1674//
1675// These factor the per-op match out of `step_apply` so the sequencing
1676// is visually separated from the arithmetic. Each helper takes the
1677// finished operand value(s) and produces a `Val` (or error), with no
1678// awareness of the kont stack — pure data-flow.
1679
1680/// Dispatch a binary operator on its evaluated operands.
1681fn dispatch_binop(
1682    op: BinOpKind,
1683    v1: Val,
1684    v2: Val,
1685    ctx: &mut EvalCtx,
1686) -> Result<Val, crate::error::NarjuError> {
1687    use crate::core::{rc_exp, tup, Exp};
1688    use crate::error::NarjuError;
1689    // lms-black's implicit Convert, in narju terms: for the
1690    // arithmetic/comparison ops, auto-lift a scalar operand when the
1691    // other side is Code, so stage-oblivious compiled code reflects
1692    // instead of erroring. Strictly widening — every case rewritten
1693    // here was an error before (including eq?'s stage-mismatch arm).
1694    let scalar_to_exp = |v: &Val| -> Option<Exp> {
1695        match v {
1696            Val::Cst(n) => Some(Exp::Lit(*n)),
1697            Val::Sym(s) => Some(Exp::Sym(s.clone())),
1698            Val::Nil => Some(Exp::Nil),
1699            _ => None,
1700        }
1701    };
1702    let (v1, v2) = match op {
1703        BinOpKind::Plus | BinOpKind::Minus | BinOpKind::Times | BinOpKind::Eq => {
1704            match (v1, v2) {
1705                (Val::Code(a), b) if !matches!(b, Val::Code(_)) => {
1706                    match scalar_to_exp(&b) {
1707                        Some(e) => (Val::Code(a), Val::Code(rc_exp(e))),
1708                        None => (Val::Code(a), b),
1709                    }
1710                }
1711                (a, Val::Code(b)) if !matches!(a, Val::Code(_)) => {
1712                    match scalar_to_exp(&a) {
1713                        Some(e) => (Val::Code(rc_exp(e)), Val::Code(b)),
1714                        None => (a, Val::Code(b)),
1715                    }
1716                }
1717                other => other,
1718            }
1719        }
1720        _ => (v1, v2),
1721    };
1722    match op {
1723        BinOpKind::CellSet => match (v1, v2) {
1724            // Staged: residualize the write (cells never fold).
1725            (Val::Code(a), Val::Code(b)) => Ok(ctx.reflectc(Exp::CellSet(a, b))),
1726            // Scope-extrusion guard: a Code value written into a real
1727            // cell could carry residual Vars out of their reify block.
1728            // Invariant: cells never contain Code.
1729            (Val::Cell(_), v2 @ Val::Code(_)) => Err(NarjuError::TypeError {
1730                op: "cell-set!",
1731                expected: "non-Code value (cells never contain Code)",
1732                actual: format!("(Cell, {})", v2.short_desc()),
1733            }),
1734            // A cell holds any run-time Val; returns the written value.
1735            (Val::Cell(i), v2) => {
1736                ctx.cells[i] = v2.clone();
1737                Ok(v2)
1738            }
1739            (a, b) => Err(NarjuError::TypeError {
1740                op: "cell-set!",
1741                expected: "Cell or matching Code",
1742                actual: format!("({}, {})", a.short_desc(), b.short_desc()),
1743            }),
1744        },
1745        BinOpKind::Plus => match (v1, v2) {
1746            (Val::Code(a), Val::Code(b)) => Ok(ctx.reflectc(Exp::Plus(
1747                a,
1748                b,
1749            ))),
1750            (Val::Cst(a), Val::Cst(b)) => Ok(Val::Cst(a + b)),
1751            (a, b) => Err(NarjuError::TypeError {
1752                op: "+",
1753                expected: "matching Cst or Code",
1754                actual: format!("({}, {})", a.short_desc(), b.short_desc()),
1755            }),
1756        },
1757        BinOpKind::Minus => match (v1, v2) {
1758            (Val::Code(a), Val::Code(b)) => Ok(ctx.reflectc(Exp::Minus(
1759                a,
1760                b,
1761            ))),
1762            (Val::Cst(a), Val::Cst(b)) => Ok(Val::Cst(a - b)),
1763            (a, b) => Err(NarjuError::TypeError {
1764                op: "-",
1765                expected: "matching Cst or Code",
1766                actual: format!("({}, {})", a.short_desc(), b.short_desc()),
1767            }),
1768        },
1769        BinOpKind::Times => match (v1, v2) {
1770            (Val::Code(a), Val::Code(b)) => Ok(ctx.reflectc(Exp::Times(
1771                a,
1772                b,
1773            ))),
1774            (Val::Cst(a), Val::Cst(b)) => Ok(Val::Cst(a * b)),
1775            (a, b) => Err(NarjuError::TypeError {
1776                op: "*",
1777                expected: "matching Cst or Code",
1778                actual: format!("({}, {})", a.short_desc(), b.short_desc()),
1779            }),
1780        },
1781        BinOpKind::Eq => match (v1, v2) {
1782            (Val::Code(a), Val::Code(b)) => Ok(ctx.reflectc(Exp::Eq(a, b))),
1783            // The paper's `binary-op` rejects stage mismatch — one
1784            // operand Code, the other not. Returning 0 instead (which
1785            // structural Eq on Val would do) hides staging bugs, so the
1786            // mismatch is an error. Scalar operands were already
1787            // auto-lifted above (lms-black's implicit Convert), so these
1788            // arms fire only for non-liftable mixes (closures, cells).
1789            (Val::Code(_), b) => Err(NarjuError::Stage(format!(
1790                "eq?: stage mismatch (Code, {})", b.short_desc()
1791            ))),
1792            (a, Val::Code(_)) => Err(NarjuError::Stage(format!(
1793                "eq?: stage mismatch ({}, Code)", a.short_desc()
1794            ))),
1795            (a, b) => Ok(Val::Cst(if a == b { 1 } else { 0 })),
1796        },
1797        BinOpKind::Cons => Ok(tup(v1, v2)),
1798
1799        BinOpKind::IsCode => match v1 {
1800            Val::Code(_) => {
1801                // Code-first-arg case is special-cased in step_apply
1802                // (BinOpFinish { op: IsCode, v1: Code }) so we can push
1803                // a ForceCode cont for v2 — force_code may drive lift-Clo
1804                // recursion through the kont, including lifting non-Code v2.
1805                unreachable!(
1806                    "IsCode with Code v1 must be special-cased in step_apply"
1807                )
1808            }
1809            _ => Ok(Val::Cst(if matches!(v2, Val::Code(_)) { 1 } else { 0 })),
1810        },
1811
1812        BinOpKind::Log => match v1 {
1813            Val::Code(_) => {
1814                // Code-first-arg case is special-cased in step_apply
1815                // (BinOpFinish { op: Log, v1: Code }) so we can push a
1816                // ForceCode cont for v2 — force_code may drive lift-Clo
1817                // recursion through the kont.
1818                unreachable!(
1819                    "Log with Code v1 must be special-cased in step_apply"
1820                )
1821            }
1822            _ => {
1823                // Unstaged: actually print, return the value unchanged.
1824                use super::display;
1825                ctx.io.print(&format!("[log] {}", display(&v2)))?;
1826                Ok(v2)
1827            }
1828        },
1829
1830        // Apps are special-cased in `step_apply` (they can produce Mode::Eval
1831        // for the Clo tail-jump branch). Reaching this arm with `App` would
1832        // mean `step_apply::BinOpFinish` failed to special-case App.
1833        BinOpKind::App => unreachable!(
1834            "BinOpKind::App must be special-cased in step_apply::BinOpFinish, \
1835             not dispatched through dispatch_binop"
1836        ),
1837    }
1838}
1839
1840/// Dispatch a unary operator on its evaluated operand.
1841fn dispatch_unop(
1842    op: UnOpKind,
1843    v: Val,
1844    ctx: &mut EvalCtx,
1845) -> Result<Val, crate::error::NarjuError> {
1846    use crate::core::{code, Exp};
1847    use crate::error::NarjuError;
1848    use super::{display, lift};
1849    match op {
1850        UnOpKind::IsNum => match v {
1851            Val::Code(s) => Ok(ctx.reflectc(Exp::IsNum(s))),
1852            Val::Cst(_) => Ok(Val::Cst(1)),
1853            _ => Ok(Val::Cst(0)),
1854        },
1855        UnOpKind::IsSym => match v {
1856            Val::Code(s) => Ok(ctx.reflectc(Exp::IsSym(s))),
1857            Val::Sym(_) => Ok(Val::Cst(1)),
1858            _ => Ok(Val::Cst(0)),
1859        },
1860        UnOpKind::IsNil => match v {
1861            Val::Code(s) => Ok(ctx.reflectc(Exp::IsNil(s))),
1862            Val::Nil => Ok(Val::Cst(1)),
1863            _ => Ok(Val::Cst(0)),
1864        },
1865        UnOpKind::IsPair => match v {
1866            Val::Code(s) => Ok(ctx.reflectc(Exp::IsPair(s))),
1867            Val::Tup(_, _) => Ok(Val::Cst(1)),
1868            _ => Ok(Val::Cst(0)),
1869        },
1870        UnOpKind::Car => match v {
1871            Val::Code(s) => Ok(ctx.reflectc(Exp::Car(s))),
1872            Val::Tup(a, _) => Ok(Rc::unwrap_or_clone(a)),
1873            other => Err(NarjuError::TypeError {
1874                op: "car",
1875                expected: "Tup",
1876                actual: other.short_desc(),
1877            }),
1878        },
1879        UnOpKind::Cdr => match v {
1880            Val::Code(s) => Ok(ctx.reflectc(Exp::Cdr(s))),
1881            Val::Tup(_, b) => Ok(Rc::unwrap_or_clone(b)),
1882            other => Err(NarjuError::TypeError {
1883                op: "cdr",
1884                expected: "Tup",
1885                actual: other.short_desc(),
1886            }),
1887        },
1888        UnOpKind::CellNew => match v {
1889            // Staged: the allocation happens when the residual runs, not
1890            // at staging time (cells never fold).
1891            Val::Code(s) => Ok(ctx.reflectc(Exp::CellNew(s))),
1892            v => Ok(ctx.cell_new(v)),
1893        },
1894        UnOpKind::CellRead => match v {
1895            // Staged: residualize the read — folding would bake the
1896            // staging-time contents into the residual.
1897            Val::Code(s) => Ok(ctx.reflectc(Exp::CellRead(s))),
1898            Val::Cell(i) => Ok(ctx.cells[i].clone()),
1899            other => Err(NarjuError::TypeError {
1900                op: "cell-read",
1901                expected: "Cell",
1902                actual: other.short_desc(),
1903            }),
1904        },
1905        UnOpKind::Lift => {
1906            let lifted = lift(v, ctx)?;
1907            Ok(code(lifted))
1908        }
1909        UnOpKind::Print => {
1910            ctx.io.print(&display(&v))?;
1911            Ok(Val::Nil)
1912        }
1913    }
1914}
1915
1916// ── Scope snapshots ───────────────────────────────────────────────────────────
1917
1918/// A snapshot of the staging-related fields of `EvalCtx`. Used to save
1919/// and restore the staging context at scope boundaries (reify, reifyc,
1920/// reifyv, run_now, Catch).
1921///
1922/// Replaces the closure-passed `run_scope` pattern of the recursive
1923/// baseline with a value we can name, hold on a continuation, and
1924/// inspect in tests. The discipline is identical: every save must be
1925/// paired with exactly one restore.
1926///
1927/// `level` is **not** part of `ScopeSnapshot` because it's saved
1928/// independently in `RunNowExit` — `level` only changes at run-now
1929/// boundaries, not at every reify scope.
1930pub struct ScopeSnapshot {
1931    /// Saved fresh-variable counter.
1932    fresh: usize,
1933    /// Saved pending-ANF block (taken via `mem::take` on save).
1934    block: Vec<Exp>,
1935    /// Saved closure memo table.
1936    fun: FunRegistry,
1937}
1938
1939impl ScopeSnapshot {
1940    /// Save the current staging state. The saved `block` is taken via
1941    /// `mem::take` — callers who want to start fresh in the new scope
1942    /// will set `ctx.block = Vec::new()` afterwards (typically via
1943    /// `save_and_reset_block` for ergonomics).
1944    ///
1945    /// `fun` is `Rc::clone`d (O(1) refcount bump, not a structural copy).
1946    pub fn save(ctx: &mut EvalCtx) -> Self {
1947        Self {
1948            fresh: ctx.fresh,
1949            block: std::mem::take(&mut ctx.block),
1950            fun: Rc::clone(&ctx.fun),
1951        }
1952    }
1953
1954    /// Save and reset block to empty. Equivalent to:
1955    ///
1956    /// ```ignore
1957    /// let snap = ScopeSnapshot::save(ctx);
1958    /// // ctx.block is already Vec::new() because mem::take left it empty.
1959    /// ```
1960    ///
1961    /// Provided for symmetry with `save` — most reify-style scopes want
1962    /// the block reset, so this is the common path. Functionally
1963    /// equivalent to plain `save` but more explicit at the call site.
1964    pub fn save_and_reset_block(ctx: &mut EvalCtx) -> Self {
1965        // mem::take leaves ctx.block as Vec::new() already.
1966        Self::save(ctx)
1967    }
1968
1969    /// Restore the saved state into `ctx`. Consumes the snapshot — each
1970    /// snapshot may be restored at most once.
1971    pub fn restore(self, ctx: &mut EvalCtx) {
1972        ctx.fresh = self.fresh;
1973        ctx.block = self.block;
1974        ctx.fun = self.fun;
1975    }
1976}
1977
1978// ── Tests ─────────────────────────────────────────────────────────────────────
1979
1980#[cfg(test)]
1981mod tests {
1982    use super::*;
1983    use crate::io::headless::HeadlessIo;
1984
1985    #[test]
1986    fn snapshot_round_trip_restores_fresh_and_block() {
1987        let mut ctx = EvalCtx::new(Box::new(HeadlessIo::empty()));
1988        ctx.fresh = 7;
1989        ctx.block.push(Exp::Lit(1));
1990        ctx.block.push(Exp::Lit(2));
1991
1992        let snap = ScopeSnapshot::save(&mut ctx);
1993
1994        // After save, block was taken — should be empty in ctx.
1995        assert_eq!(ctx.block.len(), 0);
1996        // fresh stays as it was (snapshot is a copy).
1997        assert_eq!(ctx.fresh, 7);
1998
1999        // Mutate ctx in the "scope".
2000        ctx.fresh = 42;
2001        ctx.block.push(Exp::Lit(99));
2002
2003        // Restore.
2004        snap.restore(&mut ctx);
2005
2006        assert_eq!(ctx.fresh, 7);
2007        assert_eq!(ctx.block.len(), 2);
2008        assert!(matches!(ctx.block[0], Exp::Lit(1)));
2009        assert!(matches!(ctx.block[1], Exp::Lit(2)));
2010    }
2011
2012    #[test]
2013    fn machine_new_starts_in_eval_mode_with_empty_kont() {
2014        let env: Env = Rc::new(Vec::new());
2015        let exp: RcExp = Rc::new(Exp::Lit(7));
2016        let m = Machine::new(env, exp);
2017        assert!(matches!(m.mode, Mode::Eval { .. }));
2018        assert_eq!(m.kont.len(), 0);
2019    }
2020}