narju/core/
mod.rs

1//! Core data types: expressions ([`Exp`]), values ([`Val`]), and
2//! environments ([`Env`]).
3//!
4//! The floor language is λ↑↓ as defined by base.scm in namin/pink
5//! (Amin & Rompf, *Collapsing Towers of Interpreters*, POPL 2018): a
6//! unary lambda calculus with pairs, staging operators, and a small set
7//! of effects. The representation is nameless — the parser resolves
8//! every identifier to an environment slot, so variables are
9//! [`Exp::Var`] indices and binders carry no names. This module holds
10//! only the data: the CK machine that evaluates it lives in
11//! [`crate::eval`], the reader in [`crate::parse`].
12
13use std::fmt;
14use std::rc::Rc;
15
16use smol_str::SmolStr;
17
18// ── Shared reference types ────────────────────────────────────────────────────
19//
20// All `Exp` operator children are `RcExp = Rc<Exp>` rather than `Box<Exp>`
21// so that cloning is O(1) (a ref-count bump) rather than a deep tree copy.
22//
23// This matters because the CK machine threads sub-expressions through
24// `Cont` variants (e.g. `BinOpRight { e2: RcExp }`) and `Mode::Eval`,
25// each of which needs to take an owned `Exp` out of a `&Exp` match.
26// With `Box`, that meant `*e.clone()` — a full subtree clone per
27// dispatch. With `Rc`, it's `Rc::clone(e)` — pointer-bump per dispatch.
28//
29// `Lam` body: shared across all closures created from the same `Lam` node,
30//             enabling O(1) pointer-equality in `find_fun` during staging.
31// `Code` body: shared as staged code values are threaded through staging.
32// `Tup` arms: Pink source lists (pink-eval-src etc.) are traversed many
33//             times; sharing makes car/cdr O(1) rather than deep-cloning.
34
35/// Shared expression node. See the note above on why `Rc` and not `Box`.
36pub type RcExp = Rc<Exp>;
37/// Shared value node.
38pub type RcVal = Rc<Val>;
39
40// ── Constructor helpers ───────────────────────────────────────────────────────
41
42/// Wrap an expression as a shared child node.
43#[inline] pub fn rc_exp(e: Exp) -> RcExp { Rc::new(e) }
44/// Wrap a value as a shared child node.
45#[inline] pub fn rc_val(v: Val) -> RcVal { Rc::new(v) }
46
47/// Cons two values into a [`Val::Tup`] pair.
48#[inline] pub fn tup(a: Val, b: Val) -> Val {
49    Val::Tup(rc_val(a), rc_val(b))
50}
51/// Wrap an expression tree as a [`Val::Code`] staged-code value.
52#[inline] pub fn code(e: Exp) -> Val {
53    Val::Code(rc_exp(e))
54}
55
56// ── Expression type ───────────────────────────────────────────────────────────
57
58/// A floor expression, after parsing and sugar expansion.
59///
60/// Each variant's doc gives the surface syntax it corresponds to;
61/// variants with no surface syntax ([`Exp::Proc`], [`Exp::Catch`]) say
62/// so. A common pattern throughout: primitives dispatch on whether
63/// their operands are plain values or staged [`Val::Code`] — the value
64/// case computes now, the code case emits a copy of the operation into
65/// the residual program being built (see [`crate::eval::machine`]).
66#[derive(Debug, Clone, PartialEq)]
67pub enum Exp {
68    /// Integer literal. Booleans share this encoding: the reader takes
69    /// `#t` to `Lit(1)` and `#f` to `Lit(0)`, and predicates answer 1 or 0.
70    Lit(i64),
71    /// Nil literal — the empty list / false sentinel.
72    Nil,
73    /// Symbol literal. `SmolStr` stores strings ≤22 bytes inline (no heap).
74    Sym(SmolStr),
75    /// Variable — an index into the environment, counted from its base
76    /// (a de Bruijn level, matching list position in base.scm's `env`).
77    /// The parser resolves surface names to these indices.
78    Var(usize),
79
80    /// `(lambda f x body)`: unary, self-naming lambda. Application
81    /// extends the closure's environment with the closure itself and
82    /// then the argument, so `body` reaches `f` at the first new slot
83    /// (recursion without a fixpoint combinator) and `x` at the second.
84    ///
85    /// Body as `Rc<Exp>`: all closures from the same `Lam` site share
86    /// one allocation, making `find_fun`'s ptr_eq fast path fire reliably.
87    Lam(RcExp),
88
89    /// `(f a)`: unary application. A code operand pair residualizes an
90    /// application instead of β-reducing.
91    App(RcExp, RcExp),
92    /// `(cons a b)`: pair construction.
93    Cons(RcExp, RcExp),
94    /// `(let x e body)`: evaluate `e`, bind it as the next environment
95    /// slot, evaluate `body`. The name is resolved away by the parser.
96    Let(RcExp, RcExp),
97    /// `(if c t f)`: the condition must produce a number — 0 is false,
98    /// anything else true — or code, in which case both branches are
99    /// reified and the `if` residualizes.
100    If(RcExp, RcExp, RcExp),
101
102    /// `(number? e)`: 1 if the value is a number, else 0.
103    IsNum(RcExp),
104    /// `(symbol? e)`: 1 if the value is a symbol, else 0.
105    IsSym(RcExp),
106    /// `(null? e)`: 1 if the value is nil, else 0.
107    IsNil(RcExp),
108    /// `(pair? e)`: 1 if the value is a pair, else 0.
109    IsPair(RcExp),
110    /// `(code? d e)`: 1 if `e`'s value is staged code, else 0. The
111    /// first operand is a stage dispatch: when it evaluates to code,
112    /// the test itself residualizes (strict — a non-code `e` under a
113    /// staged dispatch is an error, never an implicit lift).
114    IsCode(RcExp, RcExp),
115    /// `(car p)`: first component of a pair.
116    Car(RcExp),
117    /// `(cdr p)`: second component of a pair.
118    Cdr(RcExp),
119
120    /// `(+ a b)`: integer addition.
121    Plus(RcExp, RcExp),
122    /// `(- a b)`: integer subtraction.
123    Minus(RcExp, RcExp),
124    /// `(* a b)`: integer multiplication.
125    Times(RcExp, RcExp),
126    /// `(eq? a b)`: structural equality, answering 1 or 0. Closures and
127    /// code compare by pointer first, then structurally; cells compare
128    /// by identity.
129    Eq(RcExp, RcExp),
130
131    /// `(lift e)`: reify a value into next-stage code — numbers and
132    /// symbols become literals, pairs of code become `cons` nodes,
133    /// closures are η-expanded into staged lambdas (memoized per
134    /// closure, so recursive functions lift to a single residual
135    /// definition).
136    Lift(RcExp),
137    /// `(run b e)`: stage dispatch on `b`. Non-code `b` compiles `e`
138    /// (evaluates it in a fresh reify scope, expecting code) and
139    /// immediately executes the result; code `b` reifies `e` and
140    /// residualizes the `run` itself.
141    Run(RcExp, RcExp),
142    /// `(lift-ref name v)`: cross-stage persistence. With a non-code
143    /// first operand, `v` is evaluated and embedded in code as
144    /// [`Exp::Proc`] — the residual program references the live value
145    /// rather than a syntactic copy. With a code first operand, the
146    /// `lift-ref` residualizes.
147    LiftRef(RcExp, RcExp),
148    /// `(log b v)`: stage-aware debug print. Non-code `b`: print `v`
149    /// (as `[log] …`) and return it. Code `b`: residualize the `log`,
150    /// persisting a non-code `v` via [`Exp::Proc`].
151    Log(RcExp, RcExp),
152
153    /// Cross-stage persisted value — the reference's `(proc _ <thunk>)`
154    /// (see base.scm's `lift-ref` wrapper). Evaluates to the embedded
155    /// `Val`. Not reachable from the parser; only constructed by staged
156    /// `log`, which persists a non-code logged value into residual code
157    /// rather than coercing it to syntax.
158    Proc(RcVal),
159
160    /// `(read prompt)`: print the prompt, read one line from the
161    /// evaluator's [`NarjuIo`](crate::io::NarjuIo), parse it as a datum.
162    /// End-of-input and blank lines read as nil. Like `read-file`, this
163    /// has no Pink-source representation, so `trans` never emits it.
164    Read(RcExp),
165    /// `(read-file 'path)`: parse a source file into a list of forms-as-data
166    /// (each pre-sugged — read-file reads *source*, and `sug` is a read-time
167    /// source transform). Floor primitive for Pink-level `load`; like `Read`,
168    /// it has no Pink-source representation, so `trans` never emits it.
169    ReadFile(RcExp),
170    /// `(print e)`: write the value to the evaluator's
171    /// [`NarjuIo`](crate::io::NarjuIo); returns nil (`log` is the
172    /// variant that returns its value).
173    Print(RcExp),
174
175    /// Single restricted floor primitive for error handling in the Purple
176    /// REPL loop — not reachable from the user-facing parser.
177    Catch(RcExp),
178
179    /// `(throw v)`: evaluate `v`, then raise it as a first-class error
180    /// payload. Unwinds to the nearest `Catch`, arriving as
181    /// `(error <payload> ())`; uncaught, it propagates as a top-level
182    /// error. This is how a base env reports unbound variables loudly
183    /// instead of returning a silent zero.
184    Throw(RcExp),
185
186    /// `(evalms env-list expr)`: evaluate a code value under an
187    /// explicit environment given as a list of values. base.scm's
188    /// `evalms` exposed as a primitive — the back half of the
189    /// `trans` + `evalms` pipeline that lets floor programs animate
190    /// source-as-data (this is how lib/purple.naj boots Pink).
191    Evalms(RcExp, RcExp),
192    /// `(trans e env)`: translate an s-expression value into a code
193    /// value, resolving names positionally against `env` — a list
194    /// whose entries are symbols (plain names) or `(name . code)`
195    /// pairs (splices: the name resolves to the given code rather
196    /// than a variable). base.scm's `trans` as a primitive.
197    Trans(RcExp, RcExp),
198
199    /// `(cell-new v)`: allocate a fresh mutable cell holding v.
200    CellNew(RcExp),
201    /// `(cell-read c)`: current contents of a cell.
202    CellRead(RcExp),
203    /// `(cell-set! c v)`: write v into the cell; returns v.
204    CellSet(RcExp, RcExp),
205}
206
207/// Constructor helpers that hide `Box::new` and `rc_exp` at call sites.
208///
209/// Using these everywhere means changing an `Exp` variant's storage type
210/// (e.g. `Box` → `Rc`) only requires updating the constructor, not every
211/// call site in tests and the evaluator.
212impl Exp {
213    /// Construct [`Exp::Lam`].
214    pub fn lam(body: Exp) -> Self                      { Exp::Lam(rc_exp(body)) }
215    /// Construct [`Exp::App`].
216    pub fn app(f: Exp, a: Exp) -> Self                 { Exp::App(rc_exp(f), rc_exp(a)) }
217    /// Construct [`Exp::Let`].
218    pub fn let_(init: Exp, body: Exp) -> Self           { Exp::Let(rc_exp(init), rc_exp(body)) }
219    /// Construct [`Exp::If`].
220    pub fn if_(c: Exp, t: Exp, f: Exp) -> Self         { Exp::If(rc_exp(c), rc_exp(t), rc_exp(f)) }
221    /// Construct [`Exp::Cons`].
222    pub fn cons(a: Exp, b: Exp) -> Self                { Exp::Cons(rc_exp(a), rc_exp(b)) }
223    /// Construct [`Exp::Car`].
224    pub fn car(e: Exp) -> Self                         { Exp::Car(rc_exp(e)) }
225    /// Construct [`Exp::Cdr`].
226    pub fn cdr(e: Exp) -> Self                         { Exp::Cdr(rc_exp(e)) }
227    /// Construct [`Exp::Plus`].
228    pub fn plus(a: Exp, b: Exp) -> Self                { Exp::Plus(rc_exp(a), rc_exp(b)) }
229    /// Construct [`Exp::Minus`].
230    pub fn minus(a: Exp, b: Exp) -> Self               { Exp::Minus(rc_exp(a), rc_exp(b)) }
231    /// Construct [`Exp::Times`].
232    pub fn times(a: Exp, b: Exp) -> Self               { Exp::Times(rc_exp(a), rc_exp(b)) }
233    /// Construct [`Exp::Eq`].
234    pub fn eq(a: Exp, b: Exp) -> Self                  { Exp::Eq(rc_exp(a), rc_exp(b)) }
235    /// Construct [`Exp::IsNum`].
236    pub fn is_num(e: Exp) -> Self                      { Exp::IsNum(rc_exp(e)) }
237    /// Construct [`Exp::IsSym`].
238    pub fn is_sym(e: Exp) -> Self                      { Exp::IsSym(rc_exp(e)) }
239    /// Construct [`Exp::IsNil`].
240    pub fn is_nil(e: Exp) -> Self                      { Exp::IsNil(rc_exp(e)) }
241    /// Construct [`Exp::IsPair`].
242    pub fn is_pair(e: Exp) -> Self                     { Exp::IsPair(rc_exp(e)) }
243    /// Construct [`Exp::IsCode`].
244    pub fn is_code(dispatch: Exp, tested: Exp) -> Self { Exp::IsCode(rc_exp(dispatch), rc_exp(tested)) }
245    /// Construct [`Exp::Lift`].
246    pub fn lift(e: Exp) -> Self                        { Exp::Lift(rc_exp(e)) }
247    /// Construct [`Exp::Run`].
248    pub fn run(b: Exp, e: Exp) -> Self                 { Exp::Run(rc_exp(b), rc_exp(e)) }
249    /// Construct [`Exp::LiftRef`].
250    pub fn lift_ref(a: Exp, b: Exp) -> Self            { Exp::LiftRef(rc_exp(a), rc_exp(b)) }
251    /// Construct [`Exp::Log`].
252    pub fn log(b: Exp, v: Exp) -> Self                 { Exp::Log(rc_exp(b), rc_exp(v)) }
253    /// Construct [`Exp::Read`].
254    pub fn read(prompt: Exp) -> Self                   { Exp::Read(rc_exp(prompt)) }
255    /// Construct [`Exp::ReadFile`].
256    pub fn read_file(path: Exp) -> Self                { Exp::ReadFile(rc_exp(path)) }
257    /// Construct [`Exp::Print`].
258    pub fn print(e: Exp) -> Self                       { Exp::Print(rc_exp(e)) }
259    /// Construct [`Exp::Catch`].
260    pub fn catch(e: Exp) -> Self                       { Exp::Catch(rc_exp(e)) }
261    /// Construct [`Exp::Throw`].
262    pub fn throw(e: Exp) -> Self                       { Exp::Throw(rc_exp(e)) }
263    /// Construct [`Exp::Evalms`].
264    pub fn evalms(env_list: Exp, expr: Exp) -> Self    { Exp::Evalms(rc_exp(env_list), rc_exp(expr)) }
265    /// Construct [`Exp::Trans`].
266    pub fn trans(e: Exp, env: Exp) -> Self             { Exp::Trans(rc_exp(e), rc_exp(env)) }
267    /// Construct [`Exp::CellNew`].
268    pub fn cell_new(v: Exp) -> Self                    { Exp::CellNew(rc_exp(v)) }
269    /// Construct [`Exp::CellRead`].
270    pub fn cell_read(c: Exp) -> Self                   { Exp::CellRead(rc_exp(c)) }
271    /// Construct [`Exp::CellSet`].
272    pub fn cell_set(c: Exp, v: Exp) -> Self            { Exp::CellSet(rc_exp(c), rc_exp(v)) }
273}
274
275// ── Iterative Drop for Exp ────────────────────────────────────────────────────
276//
277// The Futamura projections produce residual programs shaped as Let-chains
278// nested thousands deep (one Let per reflected statement — see
279// EvalCtx::reifyv). A derived Drop deconstructs such a chain recursively,
280// one native stack frame per nesting level, and overflows small thread
281// stacks in debug builds (test threads default to 2 MiB). count_nodes was
282// made iterative for the same reason; Drop gets the same treatment with an
283// explicit worklist.
284//
285// Mechanism: `take_children` detaches every non-leaf RcExp child by
286// swapping in a fresh `Exp::Nil` tombstone, pushing the detached Rc onto
287// the worklist. `Rc::try_unwrap` succeeds only for uniquely-owned nodes —
288// shared nodes just get their refcount decremented, exactly as derived
289// Drop would. An unwrapped node has its own children detached before it
290// falls out of scope, so its nested Drop call sees only leaf tombstones
291// and returns without pushing — nesting depth stays constant.
292//
293// Val keeps its derived Drop: Val-linked depth (Tup list length, Clo env
294// chains) is bounded by data shape, not residual size, and any Exp tree
295// reached *through* a Val (Code, Clo body, Proc) still lands in this
296// impl when its Rc uniquely drops.
297
298impl Exp {
299    /// Detach non-leaf `RcExp` children onto `out`, leaving `Exp::Nil`
300    /// tombstones. Leaf children are left in place — their derived drop
301    /// is shallow, and skipping them keeps tombstones from re-entering
302    /// the worklist.
303    fn take_children(&mut self, out: &mut Vec<RcExp>) {
304        fn grab(slot: &mut RcExp, out: &mut Vec<RcExp>) {
305            if matches!(
306                **slot,
307                Exp::Lit(_) | Exp::Nil | Exp::Sym(_) | Exp::Var(_) | Exp::Proc(_)
308            ) {
309                return;
310            }
311            out.push(std::mem::replace(slot, rc_exp(Exp::Nil)));
312        }
313        match self {
314            Exp::Lit(_) | Exp::Sym(_) | Exp::Var(_) | Exp::Nil | Exp::Proc(_) => {}
315            Exp::Lam(b)
316            | Exp::Lift(b)
317            | Exp::Car(b)
318            | Exp::Cdr(b)
319            | Exp::IsNum(b)
320            | Exp::IsSym(b)
321            | Exp::IsNil(b)
322            | Exp::IsPair(b)
323            | Exp::Read(b)
324            | Exp::ReadFile(b)
325            | Exp::Print(b)
326            | Exp::Catch(b)
327            | Exp::Throw(b)
328            | Exp::CellNew(b)
329            | Exp::CellRead(b) => grab(b, out),
330            Exp::App(a, b)
331            | Exp::Let(a, b)
332            | Exp::Cons(a, b)
333            | Exp::Plus(a, b)
334            | Exp::Minus(a, b)
335            | Exp::Times(a, b)
336            | Exp::Eq(a, b)
337            | Exp::Run(a, b)
338            | Exp::IsCode(a, b)
339            | Exp::LiftRef(a, b)
340            | Exp::Log(a, b)
341            | Exp::Evalms(a, b)
342            | Exp::Trans(a, b)
343            | Exp::CellSet(a, b) => {
344                grab(a, out);
345                grab(b, out);
346            }
347            Exp::If(c, t, e) => {
348                grab(c, out);
349                grab(t, out);
350                grab(e, out);
351            }
352        }
353    }
354}
355
356impl Drop for Exp {
357    fn drop(&mut self) {
358        let mut stack: Vec<RcExp> = Vec::new();
359        self.take_children(&mut stack);
360        while let Some(rc) = stack.pop() {
361            if let Ok(mut e) = Rc::try_unwrap(rc) {
362                e.take_children(&mut stack);
363            }
364        }
365    }
366}
367
368// ── Value type ────────────────────────────────────────────────────────────────
369
370/// A runtime value.
371///
372/// Values and expressions are distinct types, unlike base.scm where
373/// both are s-expressions. Staged code crosses the divide as
374/// [`Val::Code`] — an expression tree held as a value — and
375/// [`Exp::Proc`] carries a value back into an expression.
376#[derive(Debug, Clone)]
377pub enum Val {
378    /// Integer constant (also the boolean encoding: 0 false, 1 true).
379    Cst(i64),
380    /// Symbol. `SmolStr` is O(1) clone for strings ≤22 bytes.
381    Sym(SmolStr),
382
383    /// Cons cell. `Rc<Val>` makes car/cdr O(1) — Pink source lists are
384    /// traversed many times during staging.
385    Tup(RcVal, RcVal),
386
387    /// Closure: environment + body.
388    /// Both are `Rc` for O(1) clone and ptr_eq in find_fun.
389    Clo(Env, RcExp),
390
391    /// The empty list, terminating every [`Val::Tup`] chain. Note that
392    /// `if` does not accept it as a condition — conditions are numbers
393    /// (or code); test for nil with `null?`.
394    Nil,
395
396    /// Staged code value. `Rc<Exp>` — shared through the staging machinery.
397    Code(RcExp),
398
399    /// First-class mutable cell — an index into `EvalCtx.cells` (the
400    /// per-instance, in-process store: each evaluator owns its own
401    /// heap, so instances snapshot and compose freely). Identity
402    /// is the index: `eq?` compares indices, `cell-set!` through one alias
403    /// is visible through all. Cells **never fold under staging**: `lift`
404    /// of a Cell errors, and staged cell ops residualize as operations.
405    Cell(usize),
406}
407
408impl PartialEq for Val {
409    fn eq(&self, other: &Self) -> bool {
410        match (self, other) {
411            (Val::Cst(a),      Val::Cst(b))      => a == b,
412            (Val::Sym(a),      Val::Sym(b))       => a == b,
413            (Val::Tup(a1, a2), Val::Tup(b1, b2)) => a1 == b1 && a2 == b2,
414            (Val::Nil,         Val::Nil)           => true,
415            (Val::Code(a),     Val::Code(b))      => Rc::ptr_eq(a, b) || a == b,
416            (Val::Clo(_, a),   Val::Clo(_, b))    => Rc::ptr_eq(a, b) || a == b,
417            (Val::Cell(a),     Val::Cell(b))      => a == b,
418            _ => false,
419        }
420    }
421}
422
423impl Val {
424    /// Compact description suitable for error messages. Avoids dumping
425    /// large `Val::Code` trees — instead reports node count.
426    ///
427    /// This is the form to use in `NarjuError::TypeError.actual` and
428    /// similar diagnostic strings. Use `display(v)` (in `eval/mod.rs`)
429    /// or `format!("{v}")` (via `fmt::Display`) for user-facing output
430    /// where the full structure matters.
431    pub fn short_desc(&self) -> String {
432        match self {
433            Val::Cst(n)    => format!("Cst({n})"),
434            Val::Sym(s)    => format!("Sym({s:?})"),
435            Val::Nil       => "Nil".to_string(),
436            Val::Clo(_, _) => "Clo(_, _)".to_string(),
437            Val::Code(e)   => format!("Code({} nodes)", count_nodes(e)),
438            Val::Tup(_, _) => "Tup(_, _)".to_string(),
439            Val::Cell(i)   => format!("Cell({i})"),
440        }
441    }
442}
443
444// ── Environment type ──────────────────────────────────────────────────────────
445
446/// A persistent, copy-on-write environment: `Rc<Vec<Val>>`.
447pub type Env = Rc<Vec<Val>>;
448
449/// Push a value onto a borrowed environment, returning a new `Env`.
450///
451/// The input `env` is borrowed, so we always need at least one `Rc::clone`
452/// to obtain an owning handle. If anyone else holds a reference to the
453/// underlying `Vec<Val>` (refcount > 1 after the clone), `Rc::make_mut`
454/// will clone the vector for copy-on-write semantics — this is the common
455/// case where a parent closure or a sibling kont frame is also looking at
456/// the same env.
457///
458/// Prefer `env_push_owned` when the caller has unique ownership of the
459/// `Env` and is about to discard it (e.g. inside `Cont::LetBody` after
460/// the cont has been popped) — that path avoids the redundant `Rc::clone`
461/// and, when refcount is 1, mutates the vector in place.
462pub fn env_push(env: &Env, val: Val) -> Env {
463    let mut new_env = Rc::clone(env);
464    Rc::make_mut(&mut new_env).push(val);
465    new_env
466}
467
468/// Push a value onto an owned environment, returning a new `Env`.
469///
470/// Consumes `env` directly, so `Rc::make_mut` sees the caller's refcount
471/// unchanged. When the caller is the sole owner (refcount == 1), this
472/// mutates the underlying `Vec<Val>` in place — no clone of the vector,
473/// no clone of the `Rc`. When other handles exist (e.g. a captured
474/// closure environment also references this `Env`), the standard
475/// copy-on-write clone still happens.
476///
477/// Used in the CK machine wherever a `Cont` arm has just taken ownership
478/// of its `env` field and is about to install a new `env` into a child
479/// `Mode::Eval` — see the `LetBody`, `EvalmsFinish`, and `BinOpFinish`
480/// `App` paths in `eval/machine.rs`.
481pub fn env_push_owned(mut env: Env, val: Val) -> Env {
482    Rc::make_mut(&mut env).push(val);
483    env
484}
485
486/// An empty environment.
487pub fn env_new() -> Env {
488    Rc::new(Vec::new())
489}
490
491// ── Display for Val ───────────────────────────────────────────────────────────
492//
493// Placed in core/ rather than eval/ so any crate layer can format a Val
494// without pulling in the evaluator. count_nodes is the only dependency on
495// Exp structure; it lives here for the same reason.
496
497/// Count Exp tree nodes — used by Val::Display for `#<code N nodes>`,
498/// and by tests that need to bound compiled-ANF size.
499///
500/// Iterative (Vec-worklist) rather than recursive, so it can handle the
501/// deeply-nested Exp trees produced by Futamura-3 self-compilation
502/// without overflowing the native stack. Each pop adds one to the count
503/// and pushes the node's children for later visitation; the final count
504/// is the total number of pops.
505pub fn count_nodes(e: &Exp) -> usize {
506    let mut count: usize = 0;
507    let mut stack: Vec<&Exp> = vec![e];
508    while let Some(node) = stack.pop() {
509        count += 1;
510        match node {
511            // Proc counts as one node: the persisted Val is a value,
512            // not syntax, so its structure doesn't contribute.
513            Exp::Lit(_) | Exp::Sym(_) | Exp::Var(_) | Exp::Nil | Exp::Proc(_) => {}
514            Exp::Lam(b)
515            | Exp::Lift(b)
516            | Exp::Car(b)
517            | Exp::Cdr(b)
518            | Exp::IsNum(b)
519            | Exp::IsSym(b)
520            | Exp::IsNil(b)
521            | Exp::IsPair(b)
522            | Exp::Read(b)
523            | Exp::ReadFile(b)
524            | Exp::Print(b)
525            | Exp::Catch(b)
526            | Exp::Throw(b)
527            | Exp::CellNew(b)
528            | Exp::CellRead(b) => stack.push(b.as_ref()),
529            Exp::App(a, b)
530            | Exp::Let(a, b)
531            | Exp::Cons(a, b)
532            | Exp::Plus(a, b)
533            | Exp::Minus(a, b)
534            | Exp::Times(a, b)
535            | Exp::Eq(a, b)
536            | Exp::Run(a, b)
537            | Exp::IsCode(a, b)
538            | Exp::LiftRef(a, b)
539            | Exp::Log(a, b)
540            | Exp::Evalms(a, b)
541            | Exp::Trans(a, b)
542            | Exp::CellSet(a, b) => {
543                stack.push(a.as_ref());
544                stack.push(b.as_ref());
545            }
546            Exp::If(c, t, e) => {
547                stack.push(c.as_ref());
548                stack.push(t.as_ref());
549                stack.push(e.as_ref());
550            }
551        }
552    }
553    count
554}
555
556impl fmt::Display for Val {
557    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558        match self {
559            Val::Cst(n)    => write!(f, "{n}"),
560            Val::Nil       => write!(f, "nil"),
561            Val::Sym(s)    => write!(f, "'{s}"),
562            Val::Clo(_, _) => write!(f, "#<closure>"),
563            Val::Code(e)   => write!(f, "#<code {} nodes>", count_nodes(e)),
564            Val::Tup(h, t) => fmt_list(f, h, t),
565            Val::Cell(i)   => write!(f, "#<cell {i}>"),
566        }
567    }
568}
569
570/// Print a `Tup` chain in list notation, falling back to dotted-pair
571/// notation for an improper tail.
572fn fmt_list(f: &mut fmt::Formatter<'_>, head: &RcVal, tail: &RcVal) -> fmt::Result {
573    write!(f, "(")?;
574    write!(f, "{head}")?;
575    let mut cur = tail.as_ref();
576    loop {
577        match cur {
578            Val::Tup(h, t) => {
579                write!(f, " {h}")?;
580                cur = t.as_ref();
581            }
582            Val::Nil => break,
583            v => { write!(f, " . {v}")?; break; }
584        }
585    }
586    write!(f, ")")
587}