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}