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}