narju/floor/
mod.rs

1pub mod host;
2pub mod machine;
3pub mod read;
4
5use std::collections::HashMap;
6use std::fmt;
7
8use smol_str::SmolStr;
9
10pub use std::sync::Arc as Shared;
11
12pub use host::Atom;
13
14pub type RcExp = Shared<Exp>;
15
16/// A newtype so the drop can be iterative: lists are unbounded in depth and
17/// derived glue overflows. On the pointer, since on `Val` it would forbid
18/// destructuring.
19#[derive(Clone)]
20pub struct RcVal(Shared<Val>);
21
22impl RcVal {
23    pub fn new(v: Val) -> RcVal {
24        RcVal(Shared::new(v))
25    }
26
27    /// Other threads only push the count up, so a low reading is a fact about
28    /// this graph rather than a race.
29    pub fn refs(&self) -> usize {
30        Shared::strong_count(&self.0)
31    }
32
33    pub fn unwrap_or_clone(mut self) -> Val {
34        match Shared::get_mut(&mut self.0) {
35            Some(v) => std::mem::replace(v, Val::Nil),
36            None => Val::clone(&self.0),
37        }
38    }
39}
40
41/// `Code` and `Clo` are not walked: they hold an [`Exp`], which detaches
42/// itself.
43fn detach(slot: &mut Shared<Val>, out: &mut Vec<RcVal>) {
44    // `get_mut` costs a compare-exchange; most drops are not of the last
45    // reference, so filter on a plain load first.
46    if Shared::strong_count(slot) != 1 {
47        return;
48    }
49    let Some(v) = Shared::get_mut(slot) else {
50        return;
51    };
52    if !matches!(v, Val::Pair(_, _) | Val::Raise(_)) {
53        return;
54    }
55    // Swapping the value out of the allocation rather than the allocation out
56    // of the slot is what keeps this allocation-free.
57    let (a, b) = match std::mem::replace(v, Val::Nil) {
58        Val::Pair(a, b) => (a, Some(b)),
59        Val::Raise(a) => (a, None),
60        _ => unreachable!("guarded above"),
61    };
62    for c in std::iter::once(a).chain(b) {
63        if matches!(&*c, Val::Pair(_, _) | Val::Raise(_)) {
64            out.push(c);
65        }
66    }
67}
68
69impl Drop for RcVal {
70    fn drop(&mut self) {
71        let mut stack: Vec<RcVal> = Vec::new();
72        detach(&mut self.0, &mut stack);
73        while let Some(mut rc) = stack.pop() {
74            // `rc` drops holding a leaf, so the recursion stops one deep.
75            detach(&mut rc.0, &mut stack);
76        }
77    }
78}
79
80impl std::ops::Deref for RcVal {
81    type Target = Val;
82    #[inline]
83    fn deref(&self) -> &Val {
84        &self.0
85    }
86}
87
88impl From<Val> for RcVal {
89    fn from(v: Val) -> RcVal {
90        RcVal::new(v)
91    }
92}
93
94impl AsRef<Val> for RcVal {
95    #[inline]
96    fn as_ref(&self) -> &Val {
97        &self.0
98    }
99}
100
101impl PartialEq for RcVal {
102    fn eq(&self, other: &RcVal) -> bool {
103        Shared::ptr_eq(&self.0, &other.0) || **self == **other
104    }
105}
106
107impl fmt::Debug for RcVal {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        fmt::Debug::fmt(&**self, f)
110    }
111}
112
113impl fmt::Display for RcVal {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        fmt::Display::fmt(&**self, f)
116    }
117}
118
119/// The value graph crosses worker threads. An `Rc` or bare `Cell` slipping
120/// into these fails the build rather than surfacing at runtime.
121const _: fn() = || {
122    fn assert_send_sync<T: Send + Sync>() {}
123    assert_send_sync::<Exp>();
124    assert_send_sync::<Val>();
125    assert_send_sync::<Env>();
126};
127
128/// Grouped under one variant because every member obeys the same staging rule
129/// (residualize if the argument is `Code`, otherwise compute), so the
130/// specializer states that rule once.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Prim1 {
133    Car,
134    Cdr,
135    IsNum,
136    IsSym,
137    IsNil,
138    IsPair,
139
140    CellNew,
141    CellRead,
142
143    Spawn,
144    /// Unary because the floor is; the argument is ignored.
145    Receive,
146    Monitor,
147    /// Not sugar: a turn ends wherever the step budget runs out, so a separate
148    /// `spawn` and `monitor` can be parted by it, letting the child die before
149    /// the watch registers and the watcher hear `noproc` rather than the cause.
150    SpawnMonitor,
151
152    /// The two exceptions to the staging rule above. A raise reaches these
153    /// rather than propagating past them, and neither residualizes - which is
154    /// what folds an interpreter's raise test away during compilation, leaving
155    /// the emitted code to get the floor's own discipline instead.
156    IsRaise,
157    RaiseValue,
158
159    /// Applying a code operator commits to the floor's calling convention,
160    /// sound only for functions the floor lifted itself. Scoped per region.
161    IsCodeFun,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Prim2 {
166    Cons,
167    Plus,
168    Minus,
169    Times,
170    Div,
171    Mod,
172    Lt,
173    Eq,
174
175    CellSet,
176    Send,
177    /// Imposed from outside because a task whose tower diverges cannot report
178    /// its own failure - reporting requires evaluating. Off by default.
179    LimitTurns,
180}
181
182impl Prim1 {
183    const NAMES: [(Prim1, &'static str); 15] = [
184        (Prim1::Car, "car"),
185        (Prim1::Cdr, "cdr"),
186        (Prim1::IsNum, "num?"),
187        (Prim1::IsSym, "sym?"),
188        (Prim1::IsNil, "nil?"),
189        (Prim1::IsPair, "pair?"),
190        (Prim1::CellNew, "cell-new"),
191        (Prim1::CellRead, "cell-read"),
192        (Prim1::Spawn, "spawn"),
193        (Prim1::Receive, "receive"),
194        (Prim1::Monitor, "monitor"),
195        (Prim1::SpawnMonitor, "spawn-monitor"),
196        (Prim1::IsRaise, "raise?"),
197        (Prim1::RaiseValue, "raise-value"),
198        (Prim1::IsCodeFun, "code-fun?"),
199    ];
200
201    pub fn from_name(s: &str) -> Option<Prim1> {
202        Prim1::NAMES.iter().find(|(_, n)| *n == s).map(|(p, _)| *p)
203    }
204
205    pub fn name(self) -> &'static str {
206        Prim1::NAMES
207            .iter()
208            .find_map(|(p, n)| (*p == self).then_some(*n))
209            .expect("every primitive is named")
210    }
211
212    /// Distinguishes what may be folded or shared from what may only be
213    /// residualized in place.
214    pub fn pure(self) -> bool {
215        !matches!(
216            self,
217            Prim1::CellNew
218                | Prim1::CellRead
219                | Prim1::Spawn
220                | Prim1::Receive
221                | Prim1::Monitor
222                | Prim1::SpawnMonitor
223        )
224    }
225}
226
227impl Prim2 {
228    const NAMES: [(Prim2, &'static str); 11] = [
229        (Prim2::Cons, "cons"),
230        (Prim2::Plus, "+"),
231        (Prim2::Minus, "-"),
232        (Prim2::Times, "*"),
233        (Prim2::Div, "/"),
234        (Prim2::Mod, "%"),
235        (Prim2::Lt, "<"),
236        (Prim2::Eq, "eq?"),
237        (Prim2::CellSet, "cell-set!"),
238        (Prim2::Send, "send"),
239        (Prim2::LimitTurns, "limit-turns"),
240    ];
241
242    pub fn from_name(s: &str) -> Option<Prim2> {
243        Prim2::NAMES.iter().find(|(_, n)| *n == s).map(|(p, _)| *p)
244    }
245
246    pub fn name(self) -> &'static str {
247        Prim2::NAMES
248            .iter()
249            .find_map(|(p, n)| (*p == self).then_some(*n))
250            .expect("every primitive is named")
251    }
252
253    pub fn pure(self) -> bool {
254        !matches!(self, Prim2::CellSet | Prim2::Send | Prim2::LimitTurns)
255    }
256}
257
258#[derive(Debug, Clone, PartialEq)]
259pub enum Exp {
260    Lit(i64),
261    /// In the core rather than behind [`host`] because boxing a
262    /// register-sized scalar is not a constant factor. Costs one match arm.
263    Flo(f64),
264    Sym(SmolStr),
265    Atom(Atom),
266    Nil,
267    /// De Bruijn index, counted from the outermost binding
268    Var(usize),
269    /// Binds its own name and then `arity` parameters, so the body sees
270    /// `arity + 1` bindings. A closure carries this node rather than just the
271    /// body, which is how the arity reaches the call.
272    Lam(u16, RcExp),
273    App(RcExp, Box<[RcExp]>),
274    /// Application at an arity not known until the call: the operands arrive
275    /// as a list value rather than as syntax, which is what an n-ary
276    /// interpreter has, and [`Exp::App`] fixes its arity when it is read.
277    Apply(RcExp, RcExp),
278    Let(RcExp, RcExp),
279    If(RcExp, RcExp, RcExp),
280
281    Prim1(Prim1, RcExp),
282    Prim2(Prim2, RcExp, RcExp),
283    /// A call into the host op table, resolved to an index by the reader
284    Op(u16, Vec<RcExp>),
285
286    Lift(RcExp),
287    /// Eta-expand at a *stated* arity, since an interpreter compiling an object
288    /// function has no floor closure to take one from. Variables as a list,
289    /// self first.
290    LiftFun(RcExp, RcExp),
291    /// Cross-stage persistence: carry a value into the next stage by
292    /// reference rather than by structure
293    LiftRef(RcExp, RcExp),
294    /// A persisted value, the residue of [`Exp::LiftRef`]. Syntax that is
295    /// not syntax: it holds a value the next stage may only pass along.
296    Proc(RcVal),
297    Run(RcExp, RcExp),
298    IsCode(RcExp, RcExp),
299
300    /// The one way to animate code compiled against a scope other than the
301    /// current one, which is what loading a module amounts to.
302    Evalms(RcExp, RcExp),
303
304    Catch(RcExp),
305    Throw(RcExp),
306}
307
308impl Exp {
309    /// The only place a new variant has to be remembered: `Drop`,
310    /// `count_nodes` and the specializer's walk all read this.
311    fn children(&self) -> ([Option<&RcExp>; 3], &[RcExp]) {
312        const NONE: [Option<&RcExp>; 3] = [None, None, None];
313        match self {
314            Exp::Lit(_)
315            | Exp::Flo(_)
316            | Exp::Sym(_)
317            | Exp::Atom(_)
318            | Exp::Nil
319            | Exp::Var(_)
320            | Exp::Proc(_) => (NONE, &[]),
321            Exp::Lam(_, a) | Exp::Prim1(_, a) | Exp::Lift(a) | Exp::Catch(a) | Exp::Throw(a) => {
322                ([Some(a), None, None], &[])
323            }
324            Exp::Let(a, b)
325            | Exp::Apply(a, b)
326            | Exp::LiftFun(a, b)
327            | Exp::Prim2(_, a, b)
328            | Exp::LiftRef(a, b)
329            | Exp::Run(a, b)
330            | Exp::IsCode(a, b)
331            | Exp::Evalms(a, b) => ([Some(a), Some(b), None], &[]),
332            Exp::If(c, t, e) => ([Some(c), Some(t), Some(e)], &[]),
333            Exp::App(f, args) => ([Some(f), None, None], args.as_ref()),
334            Exp::Op(_, args) => (NONE, args.as_slice()),
335        }
336    }
337
338    pub fn iter_children(&self) -> impl Iterator<Item = &RcExp> {
339        let (slots, rest) = self.children();
340        slots.into_iter().flatten().chain(rest)
341    }
342}
343
344/// Deep trees are ordinary in staged output, so recursive drop glue would
345/// overflow on residuals the specializer routinely produces.
346impl Drop for Exp {
347    fn drop(&mut self) {
348        let mut stack: Vec<RcExp> = Vec::new();
349        take_children(self, &mut stack);
350        while let Some(rc) = stack.pop() {
351            if let Ok(mut e) = RcExp::try_unwrap(rc) {
352                take_children(&mut e, &mut stack);
353            }
354        }
355    }
356}
357
358fn take_children(e: &mut Exp, out: &mut Vec<RcExp>) {
359    let mut grab = |slot: &mut RcExp| out.push(std::mem::replace(slot, rc_exp(Exp::Nil)));
360    match e {
361        Exp::Lit(_)
362        | Exp::Flo(_)
363        | Exp::Sym(_)
364        | Exp::Atom(_)
365        | Exp::Nil
366        | Exp::Var(_)
367        | Exp::Proc(_) => {}
368        Exp::Op(_, args) => out.append(args),
369        Exp::Lam(_, a) | Exp::Prim1(_, a) | Exp::Lift(a) | Exp::Catch(a) | Exp::Throw(a) => grab(a),
370        Exp::App(f, args) => {
371            grab(f);
372            for slot in args.iter_mut() {
373                grab(slot);
374            }
375        }
376        Exp::Let(a, b)
377        | Exp::Apply(a, b)
378        | Exp::LiftFun(a, b)
379        | Exp::Prim2(_, a, b)
380        | Exp::LiftRef(a, b)
381        | Exp::Run(a, b)
382        | Exp::IsCode(a, b)
383        | Exp::Evalms(a, b) => {
384            grab(a);
385            grab(b);
386        }
387        Exp::If(c, t, f) => {
388            grab(c);
389            grab(t);
390            grab(f);
391        }
392    }
393}
394
395pub fn count_nodes(e: &Exp) -> usize {
396    let mut count = 0usize;
397    let mut stack: Vec<&Exp> = vec![e];
398    while let Some(node) = stack.pop() {
399        count += 1;
400        stack.extend(node.iter_children().map(|c| c.as_ref()));
401    }
402    count
403}
404
405#[derive(Debug, Clone)]
406pub enum Val {
407    Num(i64),
408    Flo(f64),
409    Sym(SmolStr),
410    /// A host value. Opaque to the floor, data to a channel.
411    Atom(Atom),
412    Nil,
413    Pair(RcVal, RcVal),
414    Clo(Env, RcExp),
415    Code(RcExp),
416    /// Index into the heap's cell arena
417    Cell(usize),
418    /// A value, not a fault, so it survives being returned - `attempt` must
419    /// observe one without a floor frame, which would delimit a suspension.
420    /// Propagates where a value is inspected, inert where it is only bound.
421    Raise(RcVal),
422}
423
424pub fn raise(v: Val) -> Val {
425    Val::Raise(rc_val(v))
426}
427
428impl PartialEq for Val {
429    /// A shared allocation is skipped rather than walked, so two large values
430    /// cost their difference. The specializer's occurs check depends on it.
431    fn eq(&self, other: &Self) -> bool {
432        let mut work: Vec<(&RcVal, &RcVal)> = Vec::new();
433        let (mut a, mut b) = (self, other);
434        loop {
435            match (a, b) {
436                (Val::Num(x), Val::Num(y)) if x == y => {}
437                (Val::Flo(x), Val::Flo(y)) if x == y => {}
438                (Val::Sym(x), Val::Sym(y)) if x == y => {}
439                (Val::Atom(x), Val::Atom(y)) if x == y => {}
440                (Val::Nil, Val::Nil) => {}
441                (Val::Cell(x), Val::Cell(y)) if x == y => {}
442                (Val::Pair(x1, x2), Val::Pair(y1, y2)) => {
443                    work.push((x1, y1));
444                    work.push((x2, y2));
445                }
446                (Val::Raise(x), Val::Raise(y)) => work.push((x, y)),
447                (Val::Code(x), Val::Code(y)) | (Val::Clo(_, x), Val::Clo(_, y)) => {
448                    if !ptr_eq(x, y) && x != y {
449                        return false;
450                    }
451                }
452                _ => return false,
453            }
454            loop {
455                match work.pop() {
456                    None => return true,
457                    Some((x, y)) if ptr_eq(&x.0, &y.0) => {}
458                    Some((x, y)) => {
459                        (a, b) = (x, y);
460                        break;
461                    }
462                }
463            }
464        }
465    }
466}
467
468#[inline]
469pub fn rc_exp(e: Exp) -> RcExp {
470    RcExp::new(e)
471}
472
473#[inline]
474pub fn rc_val(v: Val) -> RcVal {
475    RcVal::new(v)
476}
477
478#[inline]
479pub fn ptr_eq<T: ?Sized>(a: &Shared<T>, b: &Shared<T>) -> bool {
480    Shared::ptr_eq(a, b)
481}
482
483#[inline]
484pub fn pair(a: Val, b: Val) -> Val {
485    Val::Pair(rc_val(a), rc_val(b))
486}
487
488#[inline]
489pub fn code(e: Exp) -> Val {
490    Val::Code(rc_exp(e))
491}
492
493pub fn list(items: &[Val]) -> Val {
494    items.iter().rev().fold(Val::Nil, |t, h| pair(h.clone(), t))
495}
496
497pub fn list_elements(v: &Val) -> Option<Vec<Val>> {
498    let mut out = Vec::new();
499    let mut cur = v;
500    loop {
501        match cur {
502            Val::Nil => return Some(out),
503            Val::Pair(h, t) => {
504                out.push((**h).clone());
505                cur = t;
506            }
507            _ => return None,
508        }
509    }
510}
511
512/// A persistent spine of chunks, each owning `[base, base + vals.len())`.
513/// Extending a shared one links rather than copies: applying a closure always
514/// shares its environment, so a flat copy-on-write vector copied every call.
515#[derive(Clone)]
516pub struct Env(Shared<EnvChunk>);
517
518/// Shape only: printing the bindings recurses through every closure they
519/// hold, burying whatever the caller was trying to read.
520impl fmt::Debug for Env {
521    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
522        write!(f, "Env(len={})", self.len())
523    }
524}
525
526struct EnvChunk {
527    parent: Option<Env>,
528    base: usize,
529    vals: Vec<Val>,
530}
531
532impl Env {
533    pub fn new(vals: Vec<Val>) -> Env {
534        Env(Shared::new(EnvChunk {
535            parent: None,
536            base: 0,
537            vals,
538        }))
539    }
540
541    pub fn len(&self) -> usize {
542        self.0.base + self.0.vals.len()
543    }
544
545    pub fn is_empty(&self) -> bool {
546        self.len() == 0
547    }
548
549    pub fn get(&self, i: usize) -> Option<&Val> {
550        let mut cur = self;
551        loop {
552            let chunk = &*cur.0;
553            if i >= chunk.base {
554                return chunk.vals.get(i - chunk.base);
555            }
556            cur = chunk.parent.as_ref()?;
557        }
558    }
559
560    /// Yields bindings in de Bruijn order. Each step re-walks the spine, so
561    /// this is for cold paths (structural equality, printing) only.
562    pub fn iter(&self) -> impl Iterator<Item = &Val> + '_ {
563        (0..self.len()).map(|i| self.get(i).expect("index below len"))
564    }
565
566    pub fn ptr_eq(a: &Env, b: &Env) -> bool {
567        Shared::ptr_eq(&a.0, &b.0)
568    }
569
570    fn link(self, val: Val) -> Env {
571        let base = self.len();
572        Env(Shared::new(EnvChunk {
573            parent: Some(self),
574            base,
575            vals: vec![val],
576        }))
577    }
578
579    /// Always links: the caller holds only a borrow, so the existing chunk may
580    /// not be disturbed.
581    pub fn push(&self, val: Val) -> Env {
582        Env::clone(self).link(val)
583    }
584
585    /// A whole frame at once. Entering a closure binds self and arguments
586    /// together, and a push apiece regrows the vector once per binding.
587    pub fn frame(self, vals: Vec<Val>) -> Env {
588        let base = self.len();
589        Env(Shared::new(EnvChunk {
590            parent: Some(self),
591            base,
592            vals,
593        }))
594    }
595
596    pub fn push_owned(mut self, val: Val) -> Env {
597        match Shared::get_mut(&mut self.0) {
598            Some(chunk) => {
599                chunk.vals.push(val);
600                self
601            }
602            None => self.link(val),
603        }
604    }
605}
606
607impl Default for Env {
608    fn default() -> Self {
609        Env::new(Vec::new())
610    }
611}
612
613/// A value is a graph: twenty pairs sharing their way down are a million nodes
614/// expanded. The refcount sieves, but a second handle may be elsewhere
615/// entirely, so the count decides rather than the refcount.
616fn repeats(v: &Val) -> HashMap<*const Val, usize> {
617    let mut seen: HashMap<*const Val, usize> = HashMap::new();
618    let mut work: Vec<&Val> = vec![v];
619    while let Some(x) = work.pop() {
620        let kids: &[&RcVal] = match x {
621            Val::Pair(a, b) => &[a, b],
622            Val::Raise(a) => &[a],
623            _ => continue,
624        };
625        for c in kids {
626            // A leaf is never worth a name: `#3=nil` is longer than what it
627            // stands for.
628            if c.refs() == 1 || !matches!(&***c, Val::Pair(_, _) | Val::Raise(_)) {
629                work.push(c);
630                continue;
631            }
632            let n = seen.entry(&***c as *const Val).or_insert(0);
633            *n += 1;
634            // Its subtree was walked the first time and has not changed.
635            if *n == 1 {
636                work.push(c);
637            }
638        }
639    }
640    seen
641}
642
643enum Say<'a> {
644    Val(&'a Val),
645    /// Tail position of a list being flattened
646    Tail(&'a Val),
647    Text(&'static str),
648}
649
650/// Iterative: a value has no depth bound and no *size* bound as a tree, so a
651/// shared subtree would exhaust memory. Answered with datum labels `#0=`/`#0#`,
652/// which [`read`] deliberately does not accept - the codec carries sharing.
653impl fmt::Display for Val {
654    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
655        let repeated = repeats(self);
656        let mut named: HashMap<*const Val, usize> = HashMap::new();
657        let mut stack = vec![Say::Val(self)];
658
659        while let Some(step) = stack.pop() {
660            let v = match step {
661                Say::Text(s) => {
662                    f.write_str(s)?;
663                    continue;
664                }
665                Say::Tail(v) => {
666                    // A named node cannot flatten into the spine: the name is
667                    // written where a value goes, so the list turns improper.
668                    if !is_repeated(v, &repeated) {
669                        match v {
670                            Val::Nil => continue,
671                            Val::Pair(h, t) => {
672                                f.write_str(" ")?;
673                                stack.push(Say::Tail(t));
674                                stack.push(Say::Val(h));
675                                continue;
676                            }
677                            _ => {}
678                        }
679                    }
680                    f.write_str(" . ")?;
681                    v
682                }
683                Say::Val(v) => v,
684            };
685
686            if is_repeated(v, &repeated) {
687                let at = v as *const Val;
688                if let Some(n) = named.get(&at) {
689                    write!(f, "#{n}#")?;
690                    continue;
691                }
692                let n = named.len();
693                named.insert(at, n);
694                write!(f, "#{n}=")?;
695            }
696
697            match v {
698                Val::Num(n) => write!(f, "{n}")?,
699                Val::Flo(x) => write!(f, "{x:?}")?,
700                Val::Nil => f.write_str("nil")?,
701                Val::Sym(s) => write!(f, "'{s}")?,
702                Val::Atom(a) => write!(f, "{a}")?,
703                Val::Clo(_, _) => f.write_str("#<closure>")?,
704                Val::Code(e) => write!(f, "#<code {e}>")?,
705                Val::Cell(i) => write!(f, "#<cell {i}>")?,
706                Val::Raise(a) => {
707                    f.write_str("#<raised ")?;
708                    stack.push(Say::Text(">"));
709                    stack.push(Say::Val(a));
710                }
711                Val::Pair(h, t) => {
712                    f.write_str("(")?;
713                    stack.push(Say::Text(")"));
714                    stack.push(Say::Tail(t));
715                    stack.push(Say::Val(h));
716                }
717            }
718        }
719        Ok(())
720    }
721}
722
723fn is_repeated(v: &Val, repeated: &HashMap<*const Val, usize>) -> bool {
724    repeated.get(&(v as *const Val)).is_some_and(|&n| n > 1)
725}
726
727/// Floor source as read, except the three things with no syntax: a de Bruijn
728/// variable, a persisted value, a host atom. Iterative for `Drop`'s reason.
729impl fmt::Display for Exp {
730    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
731        let mut stack = vec![Tok::Node(self)];
732        while let Some(tok) = stack.pop() {
733            let e = match tok {
734                Tok::Text(s) => {
735                    f.write_str(s)?;
736                    continue;
737                }
738                Tok::Node(e) => e,
739            };
740            match e {
741                Exp::Lit(n) => write!(f, "{n}")?,
742                Exp::Flo(x) => write!(f, "{x:?}")?,
743                Exp::Sym(s) => write!(f, "'{s}")?,
744                Exp::Atom(a) => write!(f, "{a}")?,
745                Exp::Nil => f.write_str("()")?,
746                Exp::Var(i) => write!(f, "x{i}")?,
747                // The bracket says "value, not source", so it goes only on the
748                // ones that would otherwise read as a literal.
749                Exp::Proc(v) => match &**v {
750                    Val::Clo(_, _) | Val::Code(_) | Val::Cell(_) | Val::Raise(_) => {
751                        write!(f, "{v}")?
752                    }
753                    v => write!(f, "#<{v}>")?,
754                },
755
756                // Binders have no names, so a unary lambda prints as bare
757                // `lambda` and any other arity says how many it takes.
758                Exp::Lam(1, b) => form(f, "lambda", &[b], &mut stack)?,
759                Exp::Lam(n, b) => {
760                    write!(f, "(lambda/{n} ")?;
761                    stack.push(Tok::Text(")"));
762                    stack.push(Tok::Node(b));
763                }
764                // An application has no head word; the operator is it.
765                Exp::App(g, args) => {
766                    stack.push(Tok::Text(")"));
767                    for a in args.iter().rev() {
768                        stack.push(Tok::Node(a));
769                        stack.push(Tok::Text(" "));
770                    }
771                    stack.push(Tok::Node(g));
772                    f.write_str("(")?;
773                }
774                Exp::Apply(g, args) => form(f, "apply", &[g, args], &mut stack)?,
775                Exp::Let(a, b) => form(f, "let", &[a, b], &mut stack)?,
776                Exp::If(c, t, o) => form(f, "if", &[c, t, o], &mut stack)?,
777                Exp::Prim1(p, a) => form(f, p.name(), &[a], &mut stack)?,
778                Exp::Prim2(p, a, b) => form(f, p.name(), &[a, b], &mut stack)?,
779                Exp::Op(idx, args) => {
780                    let kids: Vec<&RcExp> = args.iter().collect();
781                    form(f, host::registry().get(*idx).name, &kids, &mut stack)?
782                }
783                Exp::Lift(a) => form(f, "lift", &[a], &mut stack)?,
784                Exp::LiftFun(a, b) => form(f, "lift-fun", &[a, b], &mut stack)?,
785                Exp::LiftRef(a, b) => form(f, "lift-ref", &[a, b], &mut stack)?,
786                Exp::Run(a, b) => form(f, "run", &[a, b], &mut stack)?,
787                Exp::IsCode(a, b) => form(f, "code?", &[a, b], &mut stack)?,
788                Exp::Evalms(a, b) => form(f, "evalms", &[a, b], &mut stack)?,
789                Exp::Catch(a) => form(f, "catch", &[a], &mut stack)?,
790                Exp::Throw(a) => form(f, "throw", &[a], &mut stack)?,
791            }
792        }
793        Ok(())
794    }
795}
796
797enum Tok<'a> {
798    Node(&'a Exp),
799    Text(&'static str),
800}
801
802/// The stack is LIFO, so the children go on backwards.
803fn form<'a>(
804    f: &mut fmt::Formatter<'_>,
805    head: &str,
806    kids: &[&'a RcExp],
807    stack: &mut Vec<Tok<'a>>,
808) -> fmt::Result {
809    stack.push(Tok::Text(")"));
810    for k in kids.iter().rev() {
811        stack.push(Tok::Node(k));
812        stack.push(Tok::Text(" "));
813    }
814    write!(f, "({head}")
815}
816
817#[cfg(test)]
818mod tests {
819    use super::*;
820
821    #[test]
822    fn a_long_list_costs_no_stack() {
823        let build = || (0..10_000).fold(Val::Nil, |t, i| pair(Val::Num(i), t));
824        assert_eq!(build(), build());
825    }
826
827    #[test]
828    fn a_deep_car_chain_costs_no_stack() {
829        let build = || (0..10_000).fold(Val::Nil, |t, _| pair(t, Val::Nil));
830        assert_eq!(build(), build());
831    }
832
833    #[test]
834    fn printing_costs_no_stack() {
835        let deep = (0..10_000).fold(Val::Nil, |t, _| pair(t, Val::Nil));
836        assert_eq!(deep.to_string().len(), 20_003);
837        let long = (0..10_000).fold(Val::Nil, |t, _| pair(Val::Num(1), t));
838        assert_eq!(long.to_string().len(), 20_001);
839    }
840
841    #[test]
842    fn a_shared_subgraph_is_named_rather_than_repeated() {
843        let one = pair(Val::Num(1), Val::Nil);
844        let wide = (0..20).fold(one, |v, _| pair(Val::clone(&v), v));
845        let s = wide.to_string();
846        assert!(s.len() < 600, "{} chars", s.len());
847        assert!(s.starts_with("((#0=(#1="), "{s}");
848        assert!(s.ends_with(" #0# . #37#)"), "{s}");
849
850        // Sharing is a shared `RcVal`, not an equal `Val`: `pair` wraps afresh,
851        // so two clones of one value are two nodes and print as two.
852        let leaf = rc_val(pair(Val::Sym("a".into()), Val::Nil));
853        let two = |a: &RcVal, b: &RcVal| Val::Pair(RcVal::clone(a), RcVal::clone(b));
854
855        // A leaf is never named: it cannot amplify, and the name would be
856        // longer than the thing.
857        let nil = rc_val(Val::Nil);
858        assert_eq!(two(&nil, &nil).to_string(), "(nil)");
859
860        // Naming happens where a value goes, so a spine that reaches a
861        // named node stops being flattened there.
862        assert_eq!(two(&leaf, &leaf).to_string(), "(#0=('a) . #0#)");
863    }
864}