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}