narju/eval/mod.rs
1//! The evaluator: staging state ([`EvalCtx`]), the lift operator, and
2//! the entry points [`evalms`] / [`evalmsg`].
3//!
4//! Evaluation proper is the CK machine in [`machine`]; the `trans`
5//! translation from source-as-data to expressions is in [`staging`].
6//! This module holds what the machine threads through every step — the
7//! state base.scm keeps in global mutable variables (`fresh`, the open
8//! statement block, the `stFun` memoization registry, the `run` level)
9//! plus narju's additions: the cell heap and the I/O handle.
10//!
11//! The staging model follows the paper's normalization-by-evaluation
12//! reading: evaluating under `lift` produces *values of kind code*,
13//! and every reflected statement lands in the innermost open block,
14//! which `reifyv`/`reifyc` drain into ANF Let-chains when their scope
15//! closes.
16
17pub mod machine;
18pub mod staging;
19
20use std::rc::Rc;
21
22use crate::{
23 core::{code, rc_exp, Env, Exp, RcExp, Val},
24 error::NarjuError,
25 io::{terminal::TerminalIo, NarjuIo},
26};
27
28/// One stFun registry key: the `(env, body)` of a closure whose
29/// η-expansion has been memoised at some de Bruijn level. Keys hold `Rc`
30/// clones so the allocations are pinned alive for the entry's lifetime —
31/// earlier versions keyed on `Rc::as_ptr` integers and suffered stale
32/// hits after the originals were freed and their addresses reused (the
33/// F2-self-compilation OOM/wrong-value bug). Matching is *structural*
34/// (`val_equal`/`env_equal` below), following base.scm:83's `equal?` on
35/// stFun entries — so structurally identical closures reaching separate
36/// lift sites share one residual lambda.
37#[derive(Clone)]
38struct FunKey {
39 /// The closure's captured environment.
40 env: Env,
41 /// The closure's body — shared with the originating `Lam` node.
42 body: RcExp,
43}
44
45/// Full-structural value equality — the reference's `equal?` (base.scm:83
46/// matches stFun entries with `(equal? env ...)` / `(equal? e ...)`).
47///
48/// Deliberately distinct from `Val::PartialEq` (which backs the `eq?`
49/// primitive and compares closures by body only): base.scm closures are
50/// plain `(clo env body)` lists, so `equal?` recurses into the captured
51/// env as well. Using the body-only compare here would conflate closures
52/// that differ only in captured state — an incorrect memoization hit.
53///
54/// Exp comparison delegates to the derived `PartialEq`; its one Val
55/// embedding (`Exp::Proc`) routes through `Val::PartialEq`'s body-only
56/// Clo arm, but Proc cannot occur in source `Lam` bodies (it's only
57/// constructed by staged `log`), so FunKey bodies don't hit that corner
58/// in practice.
59fn val_equal(a: &Val, b: &Val) -> bool {
60 match (a, b) {
61 (Val::Cst(x), Val::Cst(y)) => x == y,
62 (Val::Sym(x), Val::Sym(y)) => x == y,
63 (Val::Nil, Val::Nil) => true,
64 (Val::Cell(x), Val::Cell(y)) => x == y,
65 (Val::Tup(a1, a2), Val::Tup(b1, b2)) => {
66 (Rc::ptr_eq(a1, b1) || val_equal(a1, b1))
67 && (Rc::ptr_eq(a2, b2) || val_equal(a2, b2))
68 }
69 (Val::Code(x), Val::Code(y)) => Rc::ptr_eq(x, y) || x == y,
70 (Val::Clo(ea, ba), Val::Clo(eb, bb)) => {
71 (Rc::ptr_eq(ba, bb) || ba == bb) && env_equal(ea, eb)
72 }
73 _ => false,
74 }
75}
76
77/// Structural equality on environments, with an `Rc::ptr_eq` fast path.
78fn env_equal(a: &Env, b: &Env) -> bool {
79 Rc::ptr_eq(a, b)
80 || (a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| val_equal(x, y)))
81}
82
83/// The stFun analogue: an assoc list scanned linearly with structural
84/// equality, exactly the reference's `List[(Int,Env,Exp)]` + `findFun`
85/// shape. Kept in an `Rc` so `ScopeSnapshot` save/restore is an O(1)
86/// refcount bump; `register_fun` copies-on-write via `Rc::make_mut`.
87type FunRegistry = Rc<Vec<(FunKey, usize)>>;
88
89/// Staging and effect state threaded through evaluation.
90///
91/// One `EvalCtx` is one evaluator instance: it owns the cell heap and
92/// the I/O handle, and carries the staging bookkeeping that base.scm
93/// keeps in global mutable variables. The staging fields (`fresh`,
94/// `block`, `fun`) are saved and restored around each reify scope;
95/// `cells`, `level`, and `io` deliberately are not — cell writes and
96/// output are effects, and `level` has its own save/restore in the
97/// `run`-now path.
98pub struct EvalCtx {
99 /// Fresh-name counter: the index the next reflected statement's
100 /// residual variable will get (the reference's `fresh`).
101 pub fresh: usize,
102 /// Statements of the innermost open reify scope, in order. Drained
103 /// into an ANF Let-chain when the scope closes.
104 pub block: Vec<Exp>,
105 /// The stFun registry; see [`FunRegistry`].
106 fun: FunRegistry,
107 /// Current `run` nesting depth: incremented for the
108 /// compile-then-execute phase of a run-now and restored after.
109 /// The raw REPL displays it in the prompt.
110 pub level: usize,
111 /// Where `read`, `print`, and unstaged `log` perform their I/O.
112 pub io: Box<dyn NarjuIo>,
113 /// First-class cell store: `Val::Cell(i)` indexes here. Cells are
114 /// per-instance and in-process by design: each evaluator owns its
115 /// heap, so instances can be snapshotted or run side by side without
116 /// sharing mutable state. Deliberately NOT part of
117 /// `ScopeSnapshot`: cell writes are effects and survive scope
118 /// save/restore — `catch` does not roll them back.
119 pub cells: Vec<Val>,
120}
121
122impl EvalCtx {
123 /// A fresh evaluator over the given I/O implementation. Use
124 /// [`crate::io::headless::HeadlessIo`] for tests and embedding;
125 /// `Default` gives a terminal-backed context and panics without a
126 /// TTY.
127 pub fn new(io: Box<dyn NarjuIo>) -> Self {
128 Self {
129 fresh: 0,
130 block: Vec::new(),
131 fun: Rc::new(Vec::new()),
132 level: 0,
133 io,
134 cells: Vec::new(),
135 }
136 }
137
138 /// Allocate a fresh cell holding `v`; returns the `Val::Cell` handle.
139 pub fn cell_new(&mut self, v: Val) -> Val {
140 self.cells.push(v);
141 Val::Cell(self.cells.len() - 1)
142 }
143
144 /// Number of memoised closures in stFun — used by :ctx command.
145 pub fn fun_len(&self) -> usize {
146 self.fun.len()
147 }
148
149 /// Allocate the next residual variable index.
150 pub fn fresh_var(&mut self) -> usize {
151 let v = self.fresh;
152 self.fresh += 1;
153 v
154 }
155
156 /// Run `f` with the staging fields saved and restored around it —
157 /// the shared skeleton under [`EvalCtx::reifyv`].
158 fn run_scope<F, T>(&mut self, f: F) -> Result<T, NarjuError>
159 where
160 F: FnOnce(&mut Self) -> Result<T, NarjuError>,
161 {
162 let saved_fresh = self.fresh;
163 let saved_block = std::mem::take(&mut self.block);
164 // O(1) Rc clone — just a reference count bump, not a Vec copy.
165 let saved_fun = Rc::clone(&self.fun);
166 let result = f(self);
167 self.fresh = saved_fresh;
168 self.block = saved_block;
169 self.fun = saved_fun;
170 result
171 }
172
173 /// Emit a statement into the current block and return the residual
174 /// variable that names its result — the paper's `reflect`. This is
175 /// where ANF comes from: every staged operation becomes one `Let`
176 /// binding when the enclosing scope drains its block.
177 pub fn reflect(&mut self, e: Exp) -> Exp {
178 self.block.push(e);
179 Exp::Var(self.fresh_var())
180 }
181
182 /// [`EvalCtx::reflect`], wrapped as a code value.
183 pub fn reflectc(&mut self, e: Exp) -> Val {
184 code(self.reflect(e))
185 }
186
187 /// Wrap a value-producing thunk in a reify-v scope: save the current
188 /// staging state, run the thunk with an empty block, then either
189 /// return the thunk's val (block empty) or wrap it in a Let-chain
190 /// (block non-empty, val must be Code).
191 ///
192 /// This is the only public reify-style method retained after the
193 /// CK rewrite. The Machine's `Cont::ReifyVExit` implements the same
194 /// logic iteratively for in-Machine reifyv scopes; this method
195 /// exists for the *top-level* entry `evalmsg`, which wraps one
196 /// Machine run in a single reifyv scope.
197 pub fn reifyv<F>(&mut self, f: F) -> Result<Val, NarjuError>
198 where
199 F: FnOnce(&mut Self) -> Result<Val, NarjuError>,
200 {
201 self.run_scope(|ctx| {
202 ctx.block = Vec::new();
203 let res = f(ctx)?;
204 let stmts = std::mem::take(&mut ctx.block);
205 if stmts.is_empty() {
206 Ok(res)
207 } else {
208 let last = match res {
209 Val::Code(e) => Rc::unwrap_or_clone(e),
210 v => {
211 return Err(NarjuError::Stage(format!(
212 "reifyv: non-empty block but non-Code result: {v:?}"
213 )))
214 }
215 };
216 let e = stmts
217 .into_iter()
218 .rev()
219 .fold(last, |acc, stmt| Exp::Let(rc_exp(stmt), rc_exp(acc)));
220 Ok(code(e))
221 }
222 })
223 }
224
225 /// Look up a closure in the stFun registry, returning the level of
226 /// its already-emitted η-expansion if present. A hit means a
227 /// recursive (or repeated) lift of the same closure resolves to a
228 /// back-reference instead of expanding forever.
229 fn find_fun(&self, env: &Env, body: &RcExp) -> Option<usize> {
230 // Body first: an `Rc::ptr_eq` hit on the same Lam node is the
231 // common case and cheap; env comparison only runs when it passes.
232 self.fun.iter().find_map(|(k, level)| {
233 let body_eq = Rc::ptr_eq(&k.body, body) || *k.body == **body;
234 if body_eq && env_equal(&k.env, env) {
235 Some(*level)
236 } else {
237 None
238 }
239 })
240 }
241
242 /// Record a closure's η-expansion level in the stFun registry
243 /// (copy-on-write, so snapshots taken before this stay intact).
244 fn register_fun(&mut self, level: usize, env: &Env, body: &RcExp) {
245 Rc::make_mut(&mut self.fun).push((
246 FunKey {
247 env: Rc::clone(env),
248 body: Rc::clone(body),
249 },
250 level,
251 ));
252 }
253}
254
255impl Default for EvalCtx {
256 fn default() -> Self {
257 let io = TerminalIo::new().unwrap_or_else(|e| panic!("failed to init terminal: {e}"));
258 Self::new(Box::new(io))
259 }
260}
261
262/// Public evaluator entry point — drives the CK machine.
263///
264/// Consumes the input `Exp` so the top-level `Rc` wrap is a `Rc::new`,
265/// not a deep `e.clone()` then wrap. Callers (in `repl::TopLevel` and
266/// tests) already own their `Exp` after `parse::lower`, so they have no
267/// reason to keep a borrow alive past this call.
268///
269/// The Machine handles every `Exp` arm natively via the kont stack;
270/// there is no recursive baseline. External callers (and `evalmsg`)
271/// go through this entry point.
272pub fn evalms(env: &Env, e: Exp, ctx: &mut EvalCtx) -> Result<Val, NarjuError> {
273 machine::Machine::new(Rc::clone(env), rc_exp(e)).run(ctx)
274}
275
276// ── NBE-style lift operator ───────────────────────────────────────────────────
277
278/// Lift a non-closure value to an Exp. Called from machine.rs's
279/// `dispatch_unop::Lift` for all variants except `Val::Clo` — the Clo
280/// case is handled directly by the CK machine via `Cont::ForceCode`
281/// driving the η-expansion through the kont stack.
282///
283/// This function is **non-recursive**: it never calls back into the
284/// evaluator. All branches are O(1) or simple constructions over already-
285/// `Code` operands.
286pub(super) fn lift(v: Val, ctx: &mut EvalCtx) -> Result<Exp, NarjuError> {
287 match v {
288 Val::Cst(n) => Ok(Exp::Lit(n)),
289 Val::Nil => Ok(Exp::Nil),
290 Val::Sym(s) => Ok(Exp::Sym(s)),
291
292 Val::Code(e) => Ok(ctx.reflect(Exp::Lift(e))),
293
294 Val::Tup(a, b) => {
295 let ca = match &*a {
296 Val::Code(e) => Rc::clone(e),
297 v => {
298 return Err(NarjuError::Stage(format!(
299 "lift Tup: car must be Code, got {v:?}"
300 )))
301 }
302 };
303 let cb = match &*b {
304 Val::Code(e) => Rc::clone(e),
305 v => {
306 return Err(NarjuError::Stage(format!(
307 "lift Tup: cdr must be Code, got {v:?}"
308 )))
309 }
310 };
311 Ok(ctx.reflect(Exp::Cons(ca, cb)))
312 }
313
314 Val::Clo(_, _) => {
315 // The CK machine's `Cont::ForceCode` handles Clo η-expansion
316 // iteratively. dispatch_unop::Lift's caller intercepts Clo
317 // before reaching this function, so this branch is dead in
318 // practice. Kept as a defensive error rather than
319 // `unreachable!` because external callers (if any) shouldn't
320 // crash the process.
321 Err(NarjuError::Stage(
322 "lift(Clo) called from non-CK path — \
323 closure η-expansion now runs via Cont::ForceCode"
324 .into(),
325 ))
326 }
327
328 // Staging rule: pure bindings fold, cells never fold.
329 // Folding a cell into residual syntax would freeze its contents
330 // at staging time. Staged cell *ops* residualize (see
331 // dispatch_unop/dispatch_binop); the cell value itself is
332 // unliftable.
333 Val::Cell(_) => Err(NarjuError::Stage(
334 "cannot lift a cell: cells never fold under staging".into(),
335 )),
336 }
337}
338// ── Top-level entry point ─────────────────────────────────────────────────────
339
340/// Paper: `evalmsg(env, e) = reifyv(lambda () evalms(env, e))`.
341///
342/// Consumes `e` and threads it into the inner closure via `move`. The
343/// previous `&Exp` signature forced a deep clone at the `evalms` entry;
344/// the new shape lets `e` flow straight through to the Machine.
345pub fn evalmsg(env: &Env, e: Exp, ctx: &mut EvalCtx) -> Result<Val, NarjuError> {
346 ctx.reifyv(move |ctx| evalms(env, e, ctx))
347}
348
349// ── Prompt formatting ─────────────────────────────────────────────────────────
350
351/// Convert a Val to a coloured REPL prompt string, matching the
352/// aesthetic of the raw λ↑↓ REPL's `make_prompt`.
353///
354/// - `Val::Sym(s)` → `"<ROSE>s<DIM>›<RESET> "`
355/// - `Val::Tup` → walks the cons-list, joining elements:
356/// `(cons 'purple (cons 0 nil))` → `"<ROSE>purple<DIM>[<BLOOM>0<DIM>]›<RESET> "`
357/// First element is the name, subsequent elements are bracketed.
358/// - anything else → bare `"<ROSE>purple<DIM>›<RESET> "`
359///
360/// ANSI escapes are emitted unconditionally — `HeadlessIo` never displays
361/// the prompt (it only returns `None` from `read_line`), and `TerminalIo`
362/// uses rustyline which handles ANSI escapes transparently.
363fn val_to_prompt(v: &Val) -> String {
364 use crate::colours::{BLOOM, DIM, RESET, ROSE};
365
366 let render = |name: &str, levels: &[String]| -> String {
367 if levels.is_empty() {
368 format!("{ROSE}{name}{DIM}›{RESET} ")
369 } else {
370 let levels = levels.join(",");
371 format!("{ROSE}{name}{DIM}[{RESET}{BLOOM}{levels}{DIM}]›{RESET} ")
372 }
373 };
374
375 match v {
376 Val::Sym(s) => render(s, &[]),
377 Val::Tup(_, _) => {
378 let mut parts = Vec::new();
379 let mut cur = v;
380 loop {
381 match cur {
382 Val::Tup(h, t) => {
383 parts.push(match h.as_ref() {
384 Val::Sym(s) => s.to_string(),
385 Val::Cst(n) => n.to_string(),
386 _ => "?".to_string(),
387 });
388 cur = t;
389 }
390 Val::Nil => break,
391 _ => break,
392 }
393 }
394 if parts.is_empty() {
395 return render("purple", &[]);
396 }
397 let name = parts[0].clone();
398 let levels: Vec<String> = parts[1..].to_vec();
399 render(&name, &levels)
400 }
401 _ => render("purple", &[]),
402 }
403}
404
405/// Render a value the way the REPL prints results. Identical to the
406/// `fmt::Display` impl on [`Val`]; kept as a named function because
407/// call sites that format for `catch` payloads and prompts read better
408/// with one.
409pub fn display(v: &Val) -> String {
410 match v {
411 Val::Cst(n) => n.to_string(),
412 Val::Nil => "nil".into(),
413 Val::Sym(s) => format!("'{s}"),
414 Val::Clo(_, _) => "#<closure>".into(),
415 Val::Code(e) => format!("#<code {} nodes>", crate::core::count_nodes(e)),
416 Val::Tup(a, b) => display_list(a, b),
417 Val::Cell(i) => format!("#<cell {i}>"),
418 }
419}
420
421/// Walk a cons-list for display, given head and tail as `RcVal`.
422fn display_list(head: &crate::core::RcVal, tail: &crate::core::RcVal) -> String {
423 let mut items = vec![display(head.as_ref())];
424 let mut cur = tail.as_ref();
425 loop {
426 match cur {
427 Val::Tup(h, t) => {
428 items.push(display(h.as_ref()));
429 cur = t.as_ref();
430 }
431 Val::Nil => return format!("({})", items.join(" ")),
432 v => return format!("({} . {})", items.join(" "), display(v)),
433 }
434 }
435}