Skip to main content

elly_core/
eval.rs

1//! A naive, tree-walking evaluator for the first Elly subset.
2//!
3//! Call-by-value (the spec defers the strategy choice). Environments are a
4//! persistent linked list of bindings shared via `Rc`, so closures capture
5//! their defining environment cheaply.
6
7use alloc::borrow::Cow;
8use alloc::boxed::Box;
9use alloc::rc::Rc;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use core::cell::{Cell, OnceCell};
13use core::cmp::Ordering;
14use core::hash::{Hash, Hasher};
15
16use num_bigint::{BigInt, Sign};
17use rpds::{HashTrieMap, Vector};
18
19use crate::ast::{
20    json_str, recur_body, Capture, Captures, Expr, HomeLeaf, Lambda, MapKey, Pattern, ProtoRef,
21    Rest, Text, TyKind,
22};
23use crate::parse::{ImportSpec, ModuleName};
24
25/// A loaded module: an immutable table of **unevaluated** item bodies
26/// ("dehydrated code"), sorted by name. Built once by [`crate::compile_module`]
27/// and then frozen behind an `Rc`. Sibling references inside the bodies are
28/// compiled to [`Expr::ModItem`] — a sink resolved through the home module carried
29/// in the environment ([`EnvNode::Module`]), not through any registry — so a
30/// `ModuleData` owns nothing but `Expr`s and never points back at a value, and
31/// the value heap stays acyclic. See `docs/done/2026-08-07_elly-modules-v0.md`.
32///
33/// A sibling reference reaches its body by `index` (a direct slice index, the hot
34/// path); an outside `M.name` access, which has only a name, binary-searches.
35#[derive(Debug, PartialEq, Eq)]
36pub struct ModuleData {
37    items: Box<[(Text, Expr)]>,
38    /// The bodies this module declares with `local` — its **private** bindings —
39    /// name-sorted. They are members exactly as the items are, materialized the
40    /// same way and addressed by the same index space, immediately after the
41    /// items: a [`ModItem`](Expr::ModItem) index at or past `items.len()` and
42    /// below [`len`](Self::len) addresses `locals[index - items.len()]`.
43    ///
44    /// **Privacy is what this second table is.** An outside `M.name` access
45    /// binary-searches the items ([`index_of`](Self::index_of)) and a sibling
46    /// reference carries an index the resolver assigned, so a table the resolver
47    /// can address and `index_of` does not search is unreachable from outside by
48    /// construction — no flag to consult and no check to run.
49    ///
50    /// The locals sit *before* the iotas rather than after them because a local's
51    /// body may be a lambda, and a materialized lambda takes its identity from
52    /// `id_base + index` — so every id-bearing member has to be inside the block
53    /// [`instantiate`] reserves. See `docs/done/2026-08-13_elly-modules-local.md`.
54    locals: Box<[(Text, Expr)]>,
55    /// The names this module declares with `const` — its **iotas** — name-sorted.
56    /// They are members alongside the items and share one address space: a
57    /// [`ModItem`](Expr::ModItem) index at or past [`len`](Self::len) addresses
58    /// `iotas[index - len()]`.
59    ///
60    /// An iota takes no frame slot and reserves no id. "Iota *i* of instance *X*"
61    /// is determined by the pair, and the declaration is inert code, so the value
62    /// is derived on reference — the same way a function item's identity is
63    /// derived from `id_base + index` rather than allocated up front. Only the
64    /// name is stored, and only so the value can render itself.
65    iotas: Box<[Text]>,
66    /// The names of the module's **frame** slots, in the order the bodies index
67    /// them — the module's import signature. Kept for the arity check in
68    /// [`instantiate`] and for a host that wants to know what a compiled module
69    /// asks to be instantiated against. Empty for a module with no frame.
70    ///
71    /// The module's own imports come first, one slot each, followed by whatever
72    /// frame names the compile was given.
73    frame_names: Box<[Text]>,
74    /// The specs of the leading frame slots — what this module imports, in slot
75    /// order, so `imports[i]` is the spec of the slot `frame_names[i]` names. The
76    /// const stage resolves each one and fills the slot with the instance (see
77    /// [`crate::load_module`]).
78    imports: Box<[ImportSpec]>,
79    /// The module's **body**: one expression evaluated in the module's own
80    /// environment, by whoever holds the module rather than by the module itself.
81    /// Two things share the slot, differing only in who evaluates it and when:
82    ///
83    /// - a [`recur`](Expr::Recur) group's body, run immediately after the
84    ///   terminal is built ([`recur`]);
85    /// - a module source's `__main`, run by a *runner* pointed at that source,
86    ///   and skipped for the same module reached by an `import`.
87    ///
88    /// It is kept beside the tables rather than appended to one as an anonymous
89    /// item because the tables are name-sorted and binary-searched, so a nameless
90    /// entry would be a hole in the one invariant `ModuleData` has. Staying out of
91    /// the member index space is also what makes it unnameable: an outside
92    /// `M.__main` is symbol projection into `items`, and there is nothing there to
93    /// find. See `docs/done/2026-08-14_elly-run.md`.
94    body: Option<Expr>,
95    /// The canonical name this code came from, when a host named it. It belongs
96    /// here rather than on the instance: it is a property of the code, and one
97    /// compiled module may be instantiated many times. `None` for a module the
98    /// host compiled without naming (and for a [`recur`](Expr::Recur) group, which
99    /// came from no source of its own).
100    name: Option<ModuleName>,
101}
102
103impl ModuleData {
104    /// Freeze a module's resolved bindings into its item table. The bindings arrive
105    /// sorted by name from [`crate::resolve::resolve_module_bodies`], which is what
106    /// the `ModItem` indexes in the bodies count against, so the order is kept.
107    pub(crate) fn from_bindings(
108        bindings: alloc::vec::Vec<(Text, Expr)>,
109        locals: alloc::vec::Vec<(Text, Expr)>,
110        iotas: Box<[Text]>,
111        frame_names: Box<[Text]>,
112        imports: Box<[ImportSpec]>,
113        body: Option<Expr>,
114        name: Option<ModuleName>,
115    ) -> ModuleData {
116        debug_assert!(
117            bindings
118                .windows(2)
119                .all(|w| w[0].0.as_str() < w[1].0.as_str()),
120            "module bindings must be sorted by name and distinct"
121        );
122        debug_assert!(
123            locals.windows(2).all(|w| w[0].0.as_str() < w[1].0.as_str()),
124            "module locals must be sorted by name and distinct"
125        );
126        debug_assert!(
127            iotas.windows(2).all(|w| w[0].as_str() < w[1].as_str()),
128            "module iotas must be sorted by name and distinct"
129        );
130        debug_assert!(
131            imports.len() <= frame_names.len(),
132            "every import is a frame slot"
133        );
134        ModuleData {
135            items: bindings.into_boxed_slice(),
136            locals: locals.into_boxed_slice(),
137            iotas,
138            frame_names,
139            imports,
140            body,
141            name,
142        }
143    }
144
145    /// The item table, name-sorted: what a [`recur`](Expr::Recur) renders and what
146    /// the resolver walks. The hot paths index it by position ([`item`](Self::item))
147    /// or binary-search it ([`index_of`](Self::index_of)) instead.
148    pub(crate) fn items(&self) -> &[(Text, Expr)] {
149        &self.items
150    }
151
152    /// The item bodies, for the resolution pass to rewrite in place. A
153    /// [`recur`](Expr::Recur)'s group is built at parse and resolved with the rest
154    /// of the tree, so its bodies are still open when the `ModuleData` exists —
155    /// unlike a file module's, which are resolved before
156    /// [`from_bindings`](Self::from_bindings) freezes them.
157    pub(crate) fn items_mut(&mut self) -> &mut [(Text, Expr)] {
158        &mut self.items
159    }
160
161    /// The body at `index` — the position a [`Expr::ModItem`] carries — covering
162    /// the items and then the [locals](Self::locals), which are one index space.
163    /// `None` for an index at or past [`len`](Self::len), which is an iota.
164    fn item(&self, index: u32) -> Option<&Expr> {
165        let index = index as usize;
166        match self.items.get(index) {
167            Some((_, body)) => Some(body),
168            None => self
169                .locals
170                .get(index - self.items.len())
171                .map(|(_, body)| body),
172        }
173    }
174
175    /// The position of `name` in the sorted table — the index an
176    /// [`Expr::ModItem`] would carry — found by binary search. An outside
177    /// `M.name` access has only a name, so it starts here and then goes through
178    /// [`item`](Self::item) like a sibling reference does.
179    fn index_of(&self, name: &str) -> Option<u32> {
180        self.items
181            .binary_search_by(|(n, _)| n.as_str().cmp(name))
182            .ok()
183            .map(|i| i as u32)
184    }
185
186    /// How many **id-bearing** members the module has — its items and its
187    /// [locals](Self::locals) — which is also where the *iota* half of the member
188    /// address space begins, and the size of the id block [`instantiate`]
189    /// reserves. Iotas mint no id, so they are not counted.
190    fn len(&self) -> usize {
191        self.items.len() + self.locals.len()
192    }
193
194    /// The name of the iota at `index`, counted from the start of the iota table
195    /// (so `member index - len()`). `None` for an index past the declarations,
196    /// which is what makes a stale member address a `.missing_property` rather
197    /// than a panic.
198    fn iota(&self, index: u32) -> Option<&Text> {
199        self.iotas.get(index as usize)
200    }
201
202    /// The module's item names, in sorted order — its **public** listing, and the
203    /// only names an outside `M.name` access can reach.
204    pub fn item_names(&self) -> Vec<&Text> {
205        self.items.iter().map(|(n, _)| n).collect()
206    }
207
208    /// The names the module declares with `local`, in sorted order. A local
209    /// appears here and nowhere else — it is not an item, so it is not reachable
210    /// as `M.name` from outside — which is the same shape a private
211    /// [iota](Self::iota_names) has, and it is offered for the same reason: a
212    /// host's introspection (a debugger, a const stage) wants to see what a
213    /// module declares, while the language gives no way to reach it.
214    pub fn local_names(&self) -> Vec<&Text> {
215        self.locals.iter().map(|(n, _)| n).collect()
216    }
217
218    /// The names the module declares with `const`, in sorted order. A **private**
219    /// iota appears here and nowhere else — it is not an item, so it is not
220    /// reachable as `M.name` from outside.
221    pub fn iota_names(&self) -> &[Text] {
222        &self.iotas
223    }
224
225    /// The names of the module's frame slots, in slot order — what it must be
226    /// instantiated against (see [`instantiate`]). Empty for a module with no
227    /// frame.
228    pub fn frame_names(&self) -> &[Text] {
229        &self.frame_names
230    }
231
232    /// The specs of the module's imports, in slot order: `imports()[i]` is what
233    /// `frame_names()[i]` was imported from. The list is a **static, auditable**
234    /// account of every module this one can reach, readable without running
235    /// anything — which is the point of writing an import rather than calling a
236    /// loader.
237    pub fn imports(&self) -> &[ImportSpec] {
238        &self.imports
239    }
240
241    /// The module's [body](Self::body), if it has one: a `recur` group's body, or
242    /// the `__main` a source declared. A runner reads it here and evaluates it in
243    /// the instance's own environment (see [`crate::eval_main`]); everything else
244    /// leaves it alone, which is what makes an *imported* `__main` inert.
245    pub fn body(&self) -> Option<&Expr> {
246        self.body.as_ref()
247    }
248
249    /// The body, for the resolution pass to rewrite in place — the counterpart of
250    /// [`items_mut`](Self::items_mut), and needed for the same reason: a
251    /// [`recur`](Expr::Recur)'s group is built at parse, so its body is still open
252    /// when the `ModuleData` exists.
253    pub(crate) fn body_mut(&mut self) -> Option<&mut Expr> {
254        self.body.as_mut()
255    }
256
257    /// The canonical name the module's code came from, when a host named it —
258    /// what `__Mod.name` reports and what the const stage deduplicates instances
259    /// by. `None` for code no host named.
260    pub fn name(&self) -> Option<&ModuleName> {
261        self.name.as_ref()
262    }
263}
264
265/// **Instantiate** a compiled module: build its [`Module`](EnvNode::Module)
266/// terminal — the environment its item bodies run in — and hand it back as the
267/// [`Value::Module`] that *is* that terminal.
268///
269/// This is where a module gets an identity. The `Rc` is fresh per call, so two
270/// instantiations of the same [`ModuleData`] are distinct objects, and one
271/// instance is equal to itself across accesses. It is also where the module's
272/// **id block** is reserved: one bump of the context's id source covering every
273/// item, so materializing item `i` can stamp its closure with the stable
274/// `id_base + i` rather than a fresh id (see `materialize_body` and
275/// `docs/done/2026-08-13_elly-modules-env.md`).
276///
277/// `frame` supplies the module's outer values, positionally, against the names
278/// [`ModuleData::frame_names`] recorded at compile time — the module's import
279/// signature, filled in. A length mismatch raises `.module_arity` rather than
280/// going through: it is memory-safe (indices are checked) but silently wrong, so
281/// it is worth catching where the mistake is. A module with no frame takes an
282/// empty slice.
283///
284/// The values are copied into a flat [`Frame`](EnvNode::Frame) node, which is the
285/// shape a fixed slot list wants. The terminal's `frame` is a plain `Env`, so a
286/// consumer that needs a cons chain instead — `recur`, whose frame *is* the
287/// enclosing lexical environment — builds the terminal directly rather than
288/// through here.
289pub fn instantiate(data: Rc<ModuleData>, frame: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
290    if frame.len() != data.frame_names.len() {
291        return Err(raise_sym("module_arity"));
292    }
293    let id_base = ctx.reserve_ids(data.len() as u64);
294    let frame = if frame.is_empty() {
295        None
296    } else {
297        Some(Rc::new(EnvNode::Frame {
298            vals: frame.to_vec().into_boxed_slice(),
299            home: None,
300        }))
301    };
302    Ok(Value::Module(Rc::new(EnvNode::Module {
303        data,
304        id_base,
305        frame,
306    })))
307}
308
309/// Evaluate a module instance's **body** — the `__main` its source declared — in
310/// the module's own environment, and hand back the result. `Ok(None)` for a
311/// module that declares none, which is what a library is.
312///
313/// This is the whole of what makes a module *runnable*, and it is a call rather
314/// than a phase of loading: [`crate::load_module`] instantiates every module it
315/// reaches and evaluates none of them, so a `__main` in an imported module is
316/// inert and only the module a runner was pointed at ever gets here. That is the
317/// Python `if __name__ == "__main__"` semantics, arrived at structurally.
318///
319/// The body runs in the instance's own [`Module`](EnvNode::Module) terminal, so
320/// it reaches the module's members exactly as an item body does. It is not a
321/// member itself, so it claims no reserved id: a closure it builds mints one from
322/// `ctx` the ordinary way. A non-module `instance` raises `.not_a_module`.
323///
324/// The result is handed back rather than interpreted. What a runner does with it
325/// — apply it to the root capability, report a still-partial closure — is the
326/// runner's policy; see `docs/done/2026-08-14_elly-run.md`.
327pub fn eval_main(instance: &Value, ctx: &Ctx) -> Result<Option<Value>, Raised> {
328    let Value::Module(home) = instance else {
329        return Err(raise_sym("not_a_module"));
330    };
331    let EnvNode::Module { data, .. } = &**home else {
332        unreachable!("a module value always holds a module terminal")
333    };
334    match data.body() {
335        Some(body) => eval_env(body, &Some(Rc::clone(home)), ctx).map(Some),
336        None => Ok(None),
337    }
338}
339
340/// The thin evaluation context threaded through the tree-walker: the unique-id
341/// source (closure identities) and the builtin module cache. It carries **no**
342/// module registry and no loader — a module's imports are resolved before
343/// evaluation begins (see [`crate::load_module`]), and a loaded module is an
344/// ordinary `Rc`-held value, kept alive only by whatever holds it.
345pub struct Ctx {
346    ids: Cell<u64>,
347    /// The instantiated [builtin modules](MODULES), one slot each, built on first
348    /// reference. Caching them here is what gives `__Int` an identity: every
349    /// reference in one evaluation yields the *same* module value, so
350    /// `__eq __Int Int` holds. Two different contexts hold non-identical `__Int`s —
351    /// v0's "two loads are unrelated modules" caveat, which a content-addressed
352    /// module brand would retire.
353    modules: [OnceCell<Value>; NMODULES],
354}
355
356impl Ctx {
357    /// A context with a fresh id source. Used by [`eval`] and [`apply`].
358    pub fn new() -> Self {
359        Ctx {
360            ids: Cell::new(0),
361            modules: core::array::from_fn(|_| OnceCell::new()),
362        }
363    }
364
365    /// A context **resuming** an id source at `ids`, so identities it mints
366    /// continue where an earlier context left off.
367    ///
368    /// A host that builds a fresh `Ctx` per call and hands values out between
369    /// them — the Python bindings do exactly this — needs it: a module reserves
370    /// its item id block when it is instantiated (see [`instantiate`]), and the
371    /// programs that later use it mint closure ids from a *different* context. If
372    /// both started at zero, an item and an unrelated closure would get the same
373    /// id and `__eq` would call them equal. The host reads
374    /// [`ids_used`](Self::ids_used) back when the context is done and seeds the
375    /// next one with it.
376    ///
377    /// The counter is carried by value rather than shared behind an `Rc`: the
378    /// pointer chase it would add to every closure mint costs ~2% on `fib_naive`,
379    /// and only the host needs the continuity.
380    pub fn with_ids(ids: u64) -> Self {
381        Ctx {
382            ids: Cell::new(ids),
383            modules: core::array::from_fn(|_| OnceCell::new()),
384        }
385    }
386
387    /// How far this context's id source has run — the value to seed a successor
388    /// with (see [`with_ids`](Self::with_ids)).
389    pub fn ids_used(&self) -> u64 {
390        self.ids.get()
391    }
392
393    /// Mint the next unique id from the runtime's id source. Each `&`-abstraction
394    /// (and each partial application) stamps its closure with one, giving callables
395    /// an identity for equality/order.
396    fn next_id(&self) -> u64 {
397        let n = self.ids.get();
398        self.ids.set(n + 1);
399        n
400    }
401
402    /// Reserve a **block** of `n` consecutive ids and return its base. Used by
403    /// [`instantiate`] to give a module's items stable identities: the ids come
404    /// from the same monotone source as [`next_id`](Self::next_id), so a block
405    /// cannot alias another block or any individually minted id.
406    fn reserve_ids(&self, n: u64) -> u64 {
407        let base = self.ids.get();
408        self.ids.set(base + n);
409        base
410    }
411
412    /// The value of a builtin module (`__Int`, `__Map`, …), instantiated on first
413    /// reference and cached for the life of the context.
414    ///
415    /// The module is an ordinary [`ModuleData`] whose item bodies are
416    /// [`Expr::Builtin`] leaves, so materializing `__Int.add` runs the same code an
417    /// `M.name` access on a loaded module runs, and yields the unsaturated
418    /// [`Value::Builtin`] a direct reference would. Nothing here is on the hot path:
419    /// a *written* `__Int.add` is folded to its member at resolve, and this is what
420    /// the remaining dynamic uses — the module as a value — go through.
421    fn builtin_module(&self, op: Builtin) -> Value {
422        let slot = op.module_slot().expect("a builtin module");
423        self.modules[slot]
424            .get_or_init(|| {
425                let items = op.items().expect("a builtin module has items");
426                let bindings = items
427                    .iter()
428                    .map(|(name, op)| (Text::from_static(name), Expr::Builtin(*op)))
429                    .collect();
430                // A builtin module is named after itself, so `__Mod.name __Int`
431                // answers `"__Int"` — the same question a loaded module answers
432                // with the name its source was resolved under.
433                let data = Rc::new(ModuleData::from_bindings(
434                    bindings,
435                    Vec::new(),
436                    Box::new([]),
437                    Box::new([]),
438                    Box::new([]),
439                    // A builtin module is a table of operations and nothing else:
440                    // there is no source to have written a `__main` in.
441                    None,
442                    Some(Text::from_static(op.name())),
443                ));
444                instantiate(data, &[], self).expect("a builtin module has no frame")
445            })
446            .clone()
447    }
448}
449
450impl Default for Ctx {
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456/// An **object**: a payload and the prototype that gives it its type. Behind the
457/// `Rc` a [`Value::Object`] carries, so an object is one payload word and a clone
458/// is one refcount bump.
459///
460/// `proto` holds exactly what a [`Value::Module`] holds — the
461/// [`Module`](EnvNode::Module) terminal that *is* the instance — so a prototype is
462/// code plus the environment that code runs in, and a method dispatched on an
463/// object runs in the same environment `M.name` would give it. It is also the same
464/// handle a [`Value::Iota`]'s `home` is, so an object and an iota answer the
465/// nominal question (`as <Module>`, `__proto x`) identically.
466///
467/// `data` is one slot holding any value. Records with named fields are a later
468/// feature; a map payload is the v0 idiom for several fields. It is reachable only
469/// from the module's own code, through [`__value`](crate::HomeLeaf::Value) — so a
470/// module may change its representation without changing its interface. See
471/// `docs/done/2026-08-15_elly-objects-v0.md`.
472#[derive(Debug)]
473pub struct ObjectData {
474    proto: Rc<EnvNode>,
475    data: Value,
476}
477
478impl ObjectData {
479    /// The object's prototype, as the module value it is.
480    pub fn proto(&self) -> Value {
481        Value::Module(Rc::clone(&self.proto))
482    }
483
484    /// The object's payload. Public to Rust because a debugger and an embedding
485    /// host sit outside the language's module scope; no *builtin* hands it out,
486    /// so Elly code cannot reach it except through its own module's `__value`.
487    pub fn data(&self) -> &Value {
488        &self.data
489    }
490}
491
492/// A runtime value.
493#[derive(Debug, Clone)]
494pub enum Value {
495    /// A machine-word integer — the **canonical** representation for any integer
496    /// that fits in `i64`. Inline (one word, no heap), so the common arithmetic
497    /// path neither allocates nor sets the enum width. The invariant "an integer
498    /// is [`Value::BigInt`] *iff* it does not fit `i64`" is what makes equality and
499    /// hashing well-defined (no value has two representations); it is maintained by
500    /// routing every wide result through [`Value::from_bigint`].
501    I64(i64),
502    /// An arbitrary-precision integer, used **only** for magnitudes outside `i64`.
503    /// Behind an `Rc` so the ~4-word `BigInt` does not widen `Value` (one word) *and*
504    /// cloning a wide value is a refcount bump rather than a digit-vector copy — the
505    /// big ints are immutable, so sharing is sound. A value here is always
506    /// `|n| > i64::MAX` by the canonical invariant (see [`Value::I64`]).
507    BigInt(Rc<BigInt>),
508    /// A symbol literal, stored *without* its dot (`foo` for `.foo`) as an owned
509    /// cheap-clone [`Text`]; the dot is re-added when displayed.
510    Symbol(Text),
511    /// An owned, immutable, cheap-clone UTF-8 string ([`Text`]). Equality and order
512    /// are Rust's `&str` (byte order over UTF-8 *is* Unicode scalar-value order, so
513    /// lexicographic comparison is a plain `str::cmp`). Rendered quoted / re-escaped.
514    Str(Text),
515    /// A runtime list of any arity, backed by an immutable `rpds::Vector`
516    /// (cheap structural-sharing clone / append, O(log n) random access). The
517    /// empty list is **unit** — there is no separate `Unit` value.
518    List(Vector<Value>),
519    /// A closure capturing its defining environment over shared `code`. `id` is a
520    /// per-runtime unique identity minted when the closure is built (an
521    /// `&`-abstraction evaluating, or a *partial* application producing a new
522    /// closure), so closures compare by *identity* (two separately-created `&x x`
523    /// are distinct, and `f x` twice yields two unequal partials) rather than by
524    /// structure — the shared `code` `Rc` is invisible to `__eq`. `code` is the
525    /// abstraction's `Lambda` (parameter patterns + body); `applied` counts the
526    /// parameters already bound into `env`, so the remaining ones are
527    /// `code.head[applied..]`. A partial application is one `Rc::clone(code)` plus
528    /// a bumped `applied` and an extended `env` — no per-partial `Lambda`
529    /// allocation. Applying the closure *matches* the next arguments against those
530    /// remaining patterns (see `apply_n`).
531    Closure {
532        id: u64,
533        code: Rc<Lambda>,
534        applied: u8,
535        env: Env,
536    },
537    /// An immutable, persistent, **unordered** map from keys to values, backed by
538    /// `rpds::HashTrieMap` (cheap structural-sharing `with`; unspecified iteration
539    /// order, stable within a build). Any value is a key; keys are identified by
540    /// structural value equality (see `impl PartialEq` / `impl Hash`). See the
541    /// *maps* section of `docs/elly-spec.md`.
542    Map(HashTrieMap<Value, Value>),
543    /// A partially-applied builtin: a native `__…` operation plus the arguments
544    /// gathered so far. Invoked once `args.len()` reaches the op's arity.
545    Builtin { op: Builtin, args: Vec<Value> },
546    /// A partially-applied **host function**: a callback the embedder supplied
547    /// ([`HostFn`]) plus the arguments gathered so far, shaped exactly like
548    /// [`Builtin`](Value::Builtin) and fired the same way — so a host callback
549    /// curries, over-applies and partially applies like everything else, with no
550    /// application path of its own.
551    ///
552    /// This is the whole FFI boundary in the calling direction, and it is a
553    /// *value* rather than an `Expr` variant deliberately: a host function
554    /// carries its own code, so it stays meaningful wherever it is handed. The
555    /// alternative — an index into a table on the [`Ctx`] — would misdispatch as
556    /// soon as a value outlived the context that made it, which a host building a
557    /// fresh `Ctx` per call does by design (see [`Ctx::with_ids`]).
558    ///
559    /// Identity is the `Rc` pointer plus the applied arguments, matching
560    /// `Builtin`'s "same operation, equal arguments". The name
561    /// [`Value::HostObj`] is deliberately left unused, for opaque host handles.
562    ///
563    /// The applied prefix is a `Box<[Value]>` rather than the `Vec` the builtin
564    /// arm carries, for width: an `Rc` plus a `Vec` is four payload words and
565    /// would take `Value` from four words to five, which the size goldens pin
566    /// (`tests/sizes.rs`) and which `docs/done/2026-08-13_elly-modules-env.md`
567    /// measured the cost of. It is not a slower shape — the builtin path clones
568    /// its `Vec` on every partial application, and `Vec::clone` allocates
569    /// `capacity == len`, so both are one allocation per curried step.
570    ///
571    /// [`Value::HostObj`]: Value
572    HostFn {
573        code: Rc<HostFn>,
574        args: Box<[Value]>,
575    },
576    /// A **module instance**: the module's own [`Module`](EnvNode::Module)
577    /// terminal — the environment its item bodies run in. The value *is* the
578    /// terminal rather than holding a [`ModuleData`]
579    /// beside one, so a module's code and the environment it was instantiated
580    /// against cannot be recombined, and materializing an item reuses this node
581    /// instead of allocating a fresh one. One payload word, so `Value` stays four
582    /// words. Applied to a symbol it *materializes* that item (see
583    /// `apply1`/`apply_n`). Identity is the `Rc` **pointer** (like a closure's
584    /// identity), not the item contents — two separate loads compare unequal; a
585    /// content-addressed brand is deferred (see
586    /// `docs/done/2026-08-13_elly-modules-env.md`).
587    Module(Rc<EnvNode>),
588    /// An **iota**: an atomic identity a module declared with `const`, equal to
589    /// itself and to nothing else. It has no structure and no payload, so there
590    /// is nothing to read out of it and nothing outside the module can conjure
591    /// one — reaching it means reaching the module.
592    ///
593    /// Identity is the pair (instance, position): `home` is the instance's
594    /// [`Module`](EnvNode::Module) terminal — the very node a [`Value::Module`]
595    /// holds — and `index` the iota's position in
596    /// [`ModuleData::iota_names`]. Nothing is minted and no id is reserved; the
597    /// value is *derived* whenever the declaration is referenced, so two
598    /// references to one iota of one instance are equal while two instantiations
599    /// of the same module mint disjoint sets. See
600    /// `docs/done/2026-08-13_elly-modules-iotas.md`.
601    Iota { home: Rc<EnvNode>, index: u32 },
602    /// An **object**: a payload plus the prototype module that gives it its type
603    /// (see [`ObjectData`]). Built only by the [`__new`](crate::HomeLeaf::New) leaf
604    /// of the module that owns the type, so a value of a type is minted by that
605    /// type's own code and nowhere else.
606    ///
607    /// Applied to a symbol it **dispatches**: the name is materialized out of the
608    /// prototype and applied to the object itself, so `x.m a` is the module's `m`
609    /// with the receiver already bound (see `apply1`/`apply_n`). Compared by
610    /// contents over the payload and by identity over the prototype, so objects
611    /// are usable as map keys.
612    Object(Rc<ObjectData>),
613}
614
615/// Structural value equality, eliminated by `__eq` (see the *equality* section of
616/// `docs/elly-spec.md`). Data compares structurally (integers mathematically,
617/// symbols/strings by text, lists elementwise, maps by content regardless of
618/// build order); callables compare by identity — builtins by operation then
619/// applied arguments, closures by their unique id. Values of different kinds are
620/// never equal. There is no total order over all values: the `<` / `>` ordering
621/// patterns compare only within `Int` / `Str` (see `order_match`).
622impl PartialEq for Value {
623    fn eq(&self, other: &Self) -> bool {
624        match (self, other) {
625            (Value::I64(a), Value::I64(b)) => a == b,
626            (Value::BigInt(a), Value::BigInt(b)) => a == b,
627            // An `I64` and a `BigInt` never compare equal: by the canonical
628            // invariant they partition the integers (a `BigInt` never fits `i64`),
629            // so they cannot denote the same number.
630            (Value::Symbol(a), Value::Symbol(b)) => a == b,
631            (Value::Str(a), Value::Str(b)) => a == b,
632            (Value::List(a), Value::List(b)) => a == b,
633            // `HashTrieMap`'s own `PartialEq` is content-based (order-independent).
634            (Value::Map(a), Value::Map(b)) => a == b,
635            // Same operation and equal applied arguments.
636            (Value::Builtin { op: o1, args: a1 }, Value::Builtin { op: o2, args: a2 }) => {
637                o1 == o2 && a1 == a2
638            }
639            // The same reading for a host callback, with the allocation standing
640            // in for the operation: a `HostFn` has no structure to compare, so
641            // two of one name are two functions.
642            (
643                Value::HostFn {
644                    code: c1, args: a1, ..
645                },
646                Value::HostFn {
647                    code: c2, args: a2, ..
648                },
649            ) => Rc::ptr_eq(c1, c2) && a1 == a2,
650            (Value::Closure { id: i1, .. }, Value::Closure { id: i2, .. }) => i1 == i2,
651            // A module value is its instance's identity: the terminal's `Rc`
652            // pointer, not the item contents (mirrors the closure "compare by
653            // identity" precedent).
654            (Value::Module(a), Value::Module(b)) => Rc::ptr_eq(a, b),
655            // An iota is the instance it was declared by plus its position, so
656            // two iotas of one instance differ and one iota of two instantiations
657            // does too.
658            (
659                Value::Iota {
660                    home: a, index: i, ..
661                },
662                Value::Iota {
663                    home: b, index: j, ..
664                },
665            ) => i == j && Rc::ptr_eq(a, b),
666            // An object is its prototype by *identity* and its payload by
667            // *contents* — the nominal/structural mix objects are for. The payload
668            // half is what makes two accesses of one data item compare equal, and
669            // what lets an object be a map key.
670            (Value::Object(a), Value::Object(b)) => {
671                Rc::ptr_eq(&a.proto, &b.proto) && a.data == b.data
672            }
673            _ => false,
674        }
675    }
676}
677
678impl Eq for Value {}
679
680/// A `Hash` consistent with the structural `PartialEq` above — required because a
681/// `Value` is a `HashTrieMap` key. A per-variant discriminant is mixed in first,
682/// so the symbol `.0`, the integer `0`, and the string `"0"` cannot collide by
683/// construction. A `Map` key hashes **order-independently** (each entry folded
684/// through a fresh sub-hasher, the results summed) so two equal maps built in
685/// different insertion orders — or with hash-colliding keys — still hash equal.
686impl Hash for Value {
687    fn hash<H: Hasher>(&self, state: &mut H) {
688        core::mem::discriminant(self).hash(state);
689        match self {
690            Value::I64(n) => n.hash(state),
691            Value::BigInt(n) => n.hash(state),
692            Value::Symbol(s) => s.hash(state),
693            Value::Str(s) => s.hash(state),
694            Value::List(vs) => {
695                for v in vs.iter() {
696                    v.hash(state);
697                }
698            }
699            Value::Map(m) => {
700                let mut acc: u64 = 0;
701                for (k, v) in m.iter() {
702                    let mut h = FnvHasher::new();
703                    k.hash(&mut h);
704                    v.hash(&mut h);
705                    acc = acc.wrapping_add(h.finish());
706                }
707                acc.hash(state);
708            }
709            Value::Builtin { op, args } => {
710                op.hash(state);
711                for a in args {
712                    a.hash(state);
713                }
714            }
715            Value::HostFn { code, args } => {
716                (Rc::as_ptr(code) as usize).hash(state);
717                for a in args {
718                    a.hash(state);
719                }
720            }
721            Value::Closure { id, .. } => id.hash(state),
722            // Hash by the same identity used for equality: the terminal's pointer.
723            Value::Module(home) => (Rc::as_ptr(home) as usize).hash(state),
724            Value::Iota { home, index } => {
725                (Rc::as_ptr(home) as usize).hash(state);
726                index.hash(state);
727            }
728            // Mirrors the equality above: the prototype's pointer, the payload's
729            // contents.
730            Value::Object(obj) => {
731                (Rc::as_ptr(&obj.proto) as usize).hash(state);
732                obj.data.hash(state);
733            }
734        }
735    }
736}
737
738/// A minimal, deterministic, no_std FNV-1a `Hasher` used only to fold a map's
739/// entries into an order-independent digest (see `impl Hash for Value`). Not the
740/// hasher the `HashTrieMap` itself uses.
741struct FnvHasher(u64);
742
743impl FnvHasher {
744    fn new() -> Self {
745        FnvHasher(0xcbf2_9ce4_8422_2325)
746    }
747}
748
749impl Hasher for FnvHasher {
750    fn finish(&self) -> u64 {
751        self.0
752    }
753    fn write(&mut self, bytes: &[u8]) {
754        for &b in bytes {
755            self.0 ^= u64::from(b);
756            self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3);
757        }
758    }
759}
760
761/// A **host function**: a native callback the embedder hands the runtime as a
762/// value, behind the `Rc` a [`Value::HostFn`] carries.
763///
764/// It is what a builtin is, minus the compile-time enum: a name, an arity, and
765/// code. The evaluator gathers arguments against `arity` and calls `f` once they
766/// are all there, so a host callback is strict and curried like everything else,
767/// and the host writes an ordinary Rust closure rather than an application
768/// protocol.
769///
770/// The `Ctx` reaches `f` so a callback that is handed an Elly callable can call
771/// it back ([`apply_with`]) under the same id source; without it a closure the
772/// callback minted would collide with one the program already holds.
773///
774/// **Arity is at least 1.** A zero-argument callback could never fire — nothing
775/// applies a value to no arguments — so a host that wants a constant supplies
776/// the value itself.
777pub struct HostFn {
778    name: Text,
779    arity: usize,
780    f: Box<HostCall>,
781}
782
783/// The contract a host callback signs: a whole call's arguments — exactly
784/// [`arity`](HostFn::arity) of them, since the evaluator gathers before it fires
785/// — plus the [`Ctx`], answering a value or a [`Raised`].
786pub type HostCall = dyn Fn(&[Value], &Ctx) -> Result<Value, Raised>;
787
788impl HostFn {
789    /// A host function called as `name`, taking `arity` arguments.
790    ///
791    /// The name is for rendering and diagnostics only — identity is the
792    /// allocation, so two `HostFn`s of one name are two functions.
793    ///
794    /// Panics on `arity == 0`, which is not a function the runtime could ever
795    /// call (see the type's docs).
796    pub fn new(
797        name: impl Into<Text>,
798        arity: usize,
799        f: impl Fn(&[Value], &Ctx) -> Result<Value, Raised> + 'static,
800    ) -> HostFn {
801        assert!(arity > 0, "a host function takes at least one argument");
802        HostFn {
803            name: name.into(),
804            arity,
805            f: Box::new(f),
806        }
807    }
808
809    /// The name the function renders under.
810    pub fn name(&self) -> &Text {
811        &self.name
812    }
813
814    /// How many arguments it is called with.
815    pub fn arity(&self) -> usize {
816        self.arity
817    }
818}
819
820/// The callback is not `Debug`, so the derive cannot apply; the shape a
821/// `Value::HostFn` shows is its name and arity, which is what identifies it in a
822/// dump anyway.
823impl core::fmt::Debug for HostFn {
824    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
825        f.debug_struct("HostFn")
826            .field("name", &self.name)
827            .field("arity", &self.arity)
828            .finish_non_exhaustive()
829    }
830}
831
832/// A native builtin operation in the reserved `__` namespace. All are curried;
833/// see the *integers* section of `docs/elly-spec.md`.
834#[allow(clippy::enum_variant_names)]
835#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
836pub enum Builtin {
837    IntAdd,
838    IntSub,
839    IntMul,
840    IntPow,
841    IntDivrem,
842    IntFor,
843    ErrRaise,
844    ErrCatch,
845    Match,
846    ListSize,
847    ListGet,
848    ListAppend,
849    ListWith,
850    ListSplit,
851    Eq,
852    MapNil,
853    MapWith,
854    MapGet,
855    MapGets,
856    MapFor,
857    MapMerge,
858    MapCat,
859    MapWithout,
860    MapSize,
861    IntStr,
862    StrPack,
863    StrUnpack,
864    SymFrom,
865    SymStr,
866    ModName,
867    /// `__proto v` — the prototype of *any* value: an object's own, and the
868    /// builtin module for every other kind. A bare builtin, like `__eq` and
869    /// `__match`, because it cuts across kinds rather than belonging to one. It is
870    /// total, so it is the classifier `as <Proto>` and the objects design are
871    /// written against, and it grants nothing: a module value carries no authority
872    /// to construct with it or to unwrap against it.
873    Proto,
874    /// The object **constructor**, `(prototype, payload)`. Not reachable by name:
875    /// it is absent from [`from_name`](Builtin::from_name) and from
876    /// [`ALL`](Builtin::ALL), and the only route to a value of it is the
877    /// [`__new`](crate::HomeLeaf::New) leaf, which supplies the prototype from the
878    /// module the body is written in. So there is no way to mint an object of a
879    /// type you are not writing.
880    ObjNew,
881    /// The object **payload reader**, `(prototype, object)` — reached the same one
882    /// way, through [`__value`](crate::HomeLeaf::Value), and refusing an object
883    /// whose prototype is not the one supplied (`.foreign_object`), so a method
884    /// cannot unwrap a value that merely passed through it.
885    ObjValue,
886    // The **builtin modules** — a 0-arity builtin each, naming the module that
887    // gathers the operations above as its items (see [`MODULES`]).
888    Int,
889    Str,
890    Sym,
891    List,
892    Map,
893    Err,
894    Mod,
895    /// `__Fun`, the prototype of every callable kind — a closure, a builtin, and
896    /// a host function. It has no items yet: `docs/todo/elly-function-arity.md`
897    /// is what gives it `nargs` / `nfree` / `nrets`. It exists for the totality of
898    /// [`Proto`](Builtin::Proto), which is enough on its own to make it a module.
899    Fun,
900}
901
902/// A builtin module's item table: `(item name, the builtin it names)`, **sorted
903/// by name** — the order a [`ModuleData`] wants, and what
904/// [`member`](Builtin::member) binary-searches.
905type Members = &'static [(&'static str, Builtin)];
906
907/// The **builtin modules**, in cache-slot order (see [`Ctx::builtin_module`]):
908/// the 0-arity builtin naming each module, its shadowable bare alias, and its
909/// items. The `__<Ns>_<op>` naming convention the flat builtins used to spell out
910/// lives here instead — `__Int.add` is the module `__Int`'s item `add` — so this
911/// table is the whole of the `__` namespace's structure.
912///
913/// `__match`, `__eq` and `__proto` are not in it: they cut across kinds and
914/// belong to no module.
915const MODULES: &[(Builtin, &str, Members)] = &[
916    (
917        Builtin::Int,
918        "Int",
919        &[
920            ("add", Builtin::IntAdd),
921            ("divrem", Builtin::IntDivrem),
922            ("for", Builtin::IntFor),
923            ("mul", Builtin::IntMul),
924            ("pow", Builtin::IntPow),
925            ("str", Builtin::IntStr),
926            ("sub", Builtin::IntSub),
927        ],
928    ),
929    (
930        Builtin::Str,
931        "Str",
932        &[("pack", Builtin::StrPack), ("unpack", Builtin::StrUnpack)],
933    ),
934    (
935        Builtin::Sym,
936        "Sym",
937        &[("from", Builtin::SymFrom), ("str", Builtin::SymStr)],
938    ),
939    (
940        Builtin::List,
941        "List",
942        &[
943            ("append", Builtin::ListAppend),
944            ("get", Builtin::ListGet),
945            ("size", Builtin::ListSize),
946            ("split", Builtin::ListSplit),
947            ("with", Builtin::ListWith),
948        ],
949    ),
950    (
951        Builtin::Map,
952        "Map",
953        &[
954            ("cat", Builtin::MapCat),
955            ("for", Builtin::MapFor),
956            ("get", Builtin::MapGet),
957            ("gets", Builtin::MapGets),
958            ("merge", Builtin::MapMerge),
959            ("nil", Builtin::MapNil),
960            ("size", Builtin::MapSize),
961            ("with", Builtin::MapWith),
962            ("without", Builtin::MapWithout),
963        ],
964    ),
965    (
966        Builtin::Err,
967        "Err",
968        &[("catch", Builtin::ErrCatch), ("raise", Builtin::ErrRaise)],
969    ),
970    (Builtin::Mod, "Mod", &[("name", Builtin::ModName)]),
971    // No items yet, and a module all the same: it is the prototype the three
972    // callable kinds answer `__proto` with (see [`Builtin::Fun`]).
973    (Builtin::Fun, "Fun", &[]),
974];
975
976/// How many builtin modules there are — the width of the [`Ctx`] cache.
977const NMODULES: usize = MODULES.len();
978
979impl Builtin {
980    /// Every **nameable** builtin, in declaration order — the source of truth for
981    /// tooling that needs to enumerate the `__` namespace (e.g. the bench suite's
982    /// coverage check). Keep in sync with the enum: a new variant forces an arm in
983    /// the exhaustive `name`/`arity` matches just below, which sit next to this
984    /// list.
985    ///
986    /// [`ObjNew`](Builtin::ObjNew) and [`ObjValue`](Builtin::ObjValue) are
987    /// deliberately absent: no name reaches them, only the home-module leaves, so
988    /// listing them would claim a `__` name that does not resolve. They still
989    /// render under one ([`name`](Builtin::name)) — a partially-applied `__new` has
990    /// to say what it is.
991    pub const ALL: &'static [Builtin] = &[
992        Builtin::IntAdd,
993        Builtin::IntSub,
994        Builtin::IntMul,
995        Builtin::IntPow,
996        Builtin::IntDivrem,
997        Builtin::IntFor,
998        Builtin::ErrRaise,
999        Builtin::ErrCatch,
1000        Builtin::Match,
1001        Builtin::ListSize,
1002        Builtin::ListGet,
1003        Builtin::ListAppend,
1004        Builtin::ListWith,
1005        Builtin::ListSplit,
1006        Builtin::Eq,
1007        Builtin::MapNil,
1008        Builtin::MapWith,
1009        Builtin::MapGet,
1010        Builtin::MapGets,
1011        Builtin::MapFor,
1012        Builtin::MapMerge,
1013        Builtin::MapCat,
1014        Builtin::MapWithout,
1015        Builtin::MapSize,
1016        Builtin::IntStr,
1017        Builtin::StrPack,
1018        Builtin::StrUnpack,
1019        Builtin::SymFrom,
1020        Builtin::SymStr,
1021        Builtin::ModName,
1022        Builtin::Proto,
1023        Builtin::Int,
1024        Builtin::Str,
1025        Builtin::Sym,
1026        Builtin::List,
1027        Builtin::Map,
1028        Builtin::Err,
1029        Builtin::Mod,
1030        Builtin::Fun,
1031    ];
1032
1033    /// Resolve a `__`-name to its builtin, if any: a bare name (`__eq`, `__match`),
1034    /// a builtin **module** (`__Int`), or a module **member** in the dotted form
1035    /// [`name`](Self::name) renders (`__Int.add`).
1036    ///
1037    /// A `<sym>` token can never contain a `.` — the dot is a Muon sigil — so the
1038    /// dotted case is not what the parser hits; it is how tooling, and the
1039    /// `Builtin::ALL` round-trip, read a rendered name back. The parser reaches a
1040    /// member through [`member`](Self::member) instead, once the resolver has folded
1041    /// `__Int .add` (see `resolve.rs`).
1042    pub fn from_name(name: &str) -> Option<Builtin> {
1043        if let Some((ns, item)) = name.split_once('.') {
1044            return Builtin::from_name(ns)?.member(item);
1045        }
1046        Some(match name {
1047            "__match" => Builtin::Match,
1048            "__eq" => Builtin::Eq,
1049            "__proto" => Builtin::Proto,
1050            "__Int" => Builtin::Int,
1051            "__Str" => Builtin::Str,
1052            "__Sym" => Builtin::Sym,
1053            "__List" => Builtin::List,
1054            "__Map" => Builtin::Map,
1055            "__Err" => Builtin::Err,
1056            "__Mod" => Builtin::Mod,
1057            "__Fun" => Builtin::Fun,
1058            _ => return None,
1059        })
1060    }
1061
1062    /// The builtin module this bare **alias** names, if any: `Int` → `__Int`. The
1063    /// aliases are the resolver's bottom tier — the language's prelude — consulted
1064    /// after everything a name could be lexically bound to, so they shadow by
1065    /// construction (see `resolve.rs`).
1066    pub fn from_alias(name: &str) -> Option<Builtin> {
1067        MODULES
1068            .iter()
1069            .find(|(_, alias, _)| *alias == name)
1070            .map(|(op, _, _)| *op)
1071    }
1072
1073    /// This builtin module's items, or `None` for anything that is not one.
1074    fn items(self) -> Option<Members> {
1075        MODULES
1076            .iter()
1077            .find(|(op, _, _)| *op == self)
1078            .map(|(_, _, items)| *items)
1079    }
1080
1081    /// The builtin module's cache slot in a [`Ctx`], or `None` for anything that is
1082    /// not one. Positional, so it is the same slot for the life of the process.
1083    fn module_slot(self) -> Option<usize> {
1084        MODULES.iter().position(|(op, _, _)| *op == self)
1085    }
1086
1087    /// The item `name` of this builtin **module**, if it has one: `__Int`'s `add`
1088    /// is `__Int.add` is [`Builtin::IntAdd`]. `None` for a name the module does not
1089    /// hold, and for any builtin that is not a module — which is what makes it safe
1090    /// to ask on every callee of a call spine (see the fold in `resolve.rs`).
1091    pub fn member(self, name: &str) -> Option<Builtin> {
1092        let items = self.items()?;
1093        items
1094            .binary_search_by(|(item, _)| (*item).cmp(name))
1095            .ok()
1096            .map(|i| items[i].1)
1097    }
1098
1099    /// The value kind this builtin **module** is the prototype of, if it is one —
1100    /// what lets an `as <Proto>` pattern naming it compile to a discriminant test
1101    /// (see [`crate::ProtoRef`]).
1102    ///
1103    /// `None` for anything that is not a module, and for `__Err`: it is a
1104    /// namespace of operations, and no value answers `__proto` with it, so `as
1105    /// Err` is rejected rather than compiled into a check that cannot hold.
1106    pub fn proto_kind(self) -> Option<TyKind> {
1107        Some(match self {
1108            Builtin::Int => TyKind::Int,
1109            Builtin::Str => TyKind::Str,
1110            Builtin::Sym => TyKind::Sym,
1111            Builtin::List => TyKind::List,
1112            Builtin::Map => TyKind::Map,
1113            Builtin::Mod => TyKind::Mod,
1114            Builtin::Fun => TyKind::Fun,
1115            _ => return None,
1116        })
1117    }
1118
1119    /// How many arguments the op consumes before it fires. A `0`-arity op is a
1120    /// bare **value**: it fires the moment its name is referenced, with no
1121    /// arguments. `__Map.nil` is the empty map; a builtin **module** is its module
1122    /// value.
1123    fn arity(self) -> usize {
1124        match self {
1125            Builtin::MapNil
1126            | Builtin::Int
1127            | Builtin::Str
1128            | Builtin::Sym
1129            | Builtin::List
1130            | Builtin::Map
1131            | Builtin::Err
1132            | Builtin::Mod
1133            | Builtin::Fun => 0,
1134            Builtin::Proto
1135            | Builtin::ErrRaise
1136            | Builtin::ListSize
1137            | Builtin::MapSize
1138            | Builtin::IntStr
1139            | Builtin::StrPack
1140            | Builtin::StrUnpack
1141            | Builtin::SymFrom
1142            | Builtin::SymStr
1143            | Builtin::ModName => 1,
1144            Builtin::IntAdd
1145            | Builtin::IntSub
1146            | Builtin::IntMul
1147            | Builtin::IntPow
1148            | Builtin::IntDivrem => 2,
1149            // `__match clauses val`: strict in the clause list and the subject.
1150            // `__new proto data` / `__value proto obj` — the prototype is the
1151            // first argument, and the home-module leaf has already supplied it.
1152            Builtin::ObjNew
1153            | Builtin::ObjValue
1154            | Builtin::Match
1155            | Builtin::ErrCatch
1156            | Builtin::ListGet
1157            | Builtin::ListAppend
1158            | Builtin::ListSplit
1159            | Builtin::MapGet
1160            | Builtin::MapGets
1161            | Builtin::MapCat
1162            | Builtin::MapWithout => 2,
1163            Builtin::ListWith | Builtin::MapWith | Builtin::MapFor | Builtin::MapMerge => 3,
1164            Builtin::IntFor | Builtin::Eq => 4,
1165        }
1166    }
1167
1168    /// Whether argument `i` is a **callback**: a value this builtin only ever
1169    /// *applies*, never stores in its result. A literal `&`-abstraction written
1170    /// straight into such a position cannot outlive the call, so the resolver gives
1171    /// it [`Captures::Chain`] — it shares the defining environment instead of
1172    /// copying a frame (see `resolve.rs`, and
1173    /// `docs/done/2026-08-14_elly-perf-closure-capture.md`).
1174    ///
1175    /// Keep this in step with the arms of [`invoke`]: an argument listed here must be
1176    /// reached only through `apply1` / `apply_n` there. Storing positions
1177    /// (`__List.with`'s element, `__Map.with`'s value) are deliberately absent — a
1178    /// closure put into a list or a map does outlive the call.
1179    pub(crate) fn applies_arg(self, i: usize) -> bool {
1180        match self {
1181            // `__Err.catch handler thunk` — both are run by the catch.
1182            Builtin::ErrCatch => i == 0 || i == 1,
1183            // `__eq a b then else` — one of the two branches is forced.
1184            Builtin::Eq => i == 2 || i == 3,
1185            // `__Int.for from to init f` — the fold callback.
1186            Builtin::IntFor => i == 3,
1187            // `__Map.for m init f` / `__Map.merge m1 m2 f` — the fold callbacks.
1188            Builtin::MapFor | Builtin::MapMerge => i == 2,
1189            _ => false,
1190        }
1191    }
1192
1193    /// Whether argument `i` is a **list of callbacks**: a list this builtin only
1194    /// applies the elements of. Only `__match`'s clause list, whose clauses are tried
1195    /// against the subject in order. When such an argument is written as a *literal*
1196    /// list, each literal abstraction in it gets [`Captures::Chain`] — this is the
1197    /// per-call closure minting the capture change made expensive.
1198    pub(crate) fn applies_elements_of(self, i: usize) -> bool {
1199        matches!(self, Builtin::Match) && i == 0
1200    }
1201
1202    /// The reserved `__`-name this builtin is referenced by: a module member is
1203    /// **dotted** (`__Int.add`), which is how it is written and how it renders.
1204    pub fn name(self) -> &'static str {
1205        match self {
1206            Builtin::IntAdd => "__Int.add",
1207            Builtin::IntSub => "__Int.sub",
1208            Builtin::IntMul => "__Int.mul",
1209            Builtin::IntPow => "__Int.pow",
1210            Builtin::IntDivrem => "__Int.divrem",
1211            Builtin::IntFor => "__Int.for",
1212            Builtin::ErrRaise => "__Err.raise",
1213            Builtin::ErrCatch => "__Err.catch",
1214            Builtin::Match => "__match",
1215            Builtin::ListSize => "__List.size",
1216            Builtin::ListGet => "__List.get",
1217            Builtin::ListAppend => "__List.append",
1218            Builtin::ListWith => "__List.with",
1219            Builtin::ListSplit => "__List.split",
1220            Builtin::Eq => "__eq",
1221            Builtin::MapNil => "__Map.nil",
1222            Builtin::MapWith => "__Map.with",
1223            Builtin::MapGet => "__Map.get",
1224            Builtin::MapGets => "__Map.gets",
1225            Builtin::MapFor => "__Map.for",
1226            Builtin::MapMerge => "__Map.merge",
1227            Builtin::MapCat => "__Map.cat",
1228            Builtin::MapWithout => "__Map.without",
1229            Builtin::MapSize => "__Map.size",
1230            Builtin::IntStr => "__Int.str",
1231            Builtin::StrPack => "__Str.pack",
1232            Builtin::StrUnpack => "__Str.unpack",
1233            Builtin::SymFrom => "__Sym.from",
1234            Builtin::SymStr => "__Sym.str",
1235            Builtin::ModName => "__Mod.name",
1236            Builtin::Proto => "__proto",
1237            // The two capabilities render under the leaf that produced them —
1238            // there is no other way to hold one, so this is the name a reader
1239            // wrote (see [`ALL`](Builtin::ALL) for why they are not in it).
1240            Builtin::ObjNew => "__new",
1241            Builtin::ObjValue => "__value",
1242            Builtin::Int => "__Int",
1243            Builtin::Str => "__Str",
1244            Builtin::Sym => "__Sym",
1245            Builtin::List => "__List",
1246            Builtin::Map => "__Map",
1247            Builtin::Err => "__Err",
1248            Builtin::Mod => "__Mod",
1249            Builtin::Fun => "__Fun",
1250        }
1251    }
1252}
1253
1254/// A shared, persistent environment: a **nameless** cons list of the bindings the
1255/// running activation has made (innermost/most-recent first), on top of the
1256/// closure's captured [`Frame`](EnvNode::Frame). A resolved reference
1257/// ([`Expr::Local`](crate::Expr)) reaches its value by walking its de Bruijn
1258/// `index` `next` links and cloning the cell's value — a pointer chase, no name
1259/// compare and no per-link clone (contrast the old name-keyed cons list) — and an
1260/// index that runs past the consed cells lands in the frame, an array index. The
1261/// resolver (`resolve.rs`) assigns each reference the exact number of cells
1262/// between it and its binder, in the same order the matcher conses them.
1263pub type Env = Option<Rc<EnvNode>>;
1264
1265/// One link of the environment list: a single binding carrying its `val` inline,
1266/// or the frame that terminates the walk. The empty environment is `None`, not a
1267/// node — [`Env`] is an `Option<Rc<EnvNode>>`, which the null-pointer niche keeps
1268/// one word wide, so ending a chain costs no allocation and no refcount traffic. A lambda activation conses one `Cons`
1269/// per name its head binds — zero for a binder-less head (`&_`, `&(< 2)`) — so a
1270/// partial application's environment is just the consed prefix, shared with later
1271/// partials by `Rc` refcount (no copy-on-write). Names are not stored: references
1272/// were resolved to a de Bruijn index against this list's shape.
1273#[derive(Debug)]
1274pub enum EnvNode {
1275    Cons {
1276        val: Value,
1277        next: Env,
1278    },
1279    /// The **home terminal** of a module item's environment: it carries the item's
1280    /// module in place of the empty terminal, so a [`Expr::ModItem`] sibling
1281    /// reference resolves
1282    /// by walking out to it (see the `ModItem` arm of [`eval_env`]). It binds no
1283    /// name and is not counted by de Bruijn `Local` indices — a valid index always
1284    /// lands on a `Cons` or a `Frame` before reaching here. A materialized closure
1285    /// captures an environment ending in this node, which is how it keeps its module
1286    /// alive (an `Rc` edge closure → module, with no edge back — see
1287    /// `docs/done/2026-08-07_elly-modules-v0.md`).
1288    ///
1289    /// This node **is** the module instance: [`Value::Module`] is a handle on it,
1290    /// minted by [`instantiate`]. So the value and the environment its items run
1291    /// in are one allocation, and `M.name` from outside runs the body in exactly
1292    /// the environment a sibling reference would.
1293    ///
1294    /// `id_base` is the module's reserved id block: item `i` stamps the closure it
1295    /// materializes to with `id_base + i`, so `M.f == M.f` (see
1296    /// `materialize_body`).
1297    ///
1298    /// `frame` is the environment the module was **instantiated against** — its
1299    /// imports, host prelude and module-minted identities, in the slot order
1300    /// [`ModuleData::frame_names`] fixed at compile time. A de Bruijn index walks
1301    /// *through* this node into `frame` **without counting it**, so a frame
1302    /// reference is an ordinary [`Expr::Local`] and needs no variant of its own.
1303    /// The node binds no name itself, which is what makes passing the walk on
1304    /// consistent rather than an off-by-one.
1305    ///
1306    /// The shape of `frame` is deliberately left open: it is an `Env`, so a flat
1307    /// [`Frame`](EnvNode::Frame) (named imports — a fixed slot list) and a cons
1308    /// chain (`recur` — the frame *is* the enclosing lexical environment) are both
1309    /// reachable through one code path. `None` is a module with no frame, which is
1310    /// every module compiled by [`crate::compile_module`].
1311    ///
1312    /// Holding an environment is what gives up modules v0's *structural*
1313    /// acyclicity — a `ModuleData` could hold no `Value`, so no cycle was
1314    /// expressible — for a
1315    /// *temporal* one: a frame holds only values that existed before the frame did,
1316    /// and immutability means none of them can later be made to point back at it.
1317    /// See `docs/done/2026-08-13_elly-modules-env.md`.
1318    Module {
1319        data: Rc<ModuleData>,
1320        id_base: u64,
1321        frame: Env,
1322    },
1323    /// A closure's captured **frame**: the values of the free variables its body
1324    /// actually uses, copied in at closure creation in the slot order
1325    /// `Lambda::captures` fixed at resolve. It **terminates**
1326    /// the de Bruijn walk: an index that runs off the consed cells selects
1327    /// `vals[remaining]` (see `lookup`), so a reference into an enclosing scope
1328    /// costs a walk over the activation's own bindings plus one array index —
1329    /// never a walk over the whole enclosing program — and a closure retains only
1330    /// what it uses.
1331    ///
1332    /// `home` carries the environment's terminal (`None`, or the
1333    /// [`Module`](EnvNode::Module) node of
1334    /// the module item this closure came from) on past the frame, so a `ModItem`
1335    /// inside the body still finds its module — and finds the *same* node, so
1336    /// materializing an item allocates no environment. Nothing addresses `home` by
1337    /// index: the frame is the last indexable node.
1338    ///
1339    /// The values live inline behind the environment's own `Rc`, so a frame is one
1340    /// allocation and is shared by refcount exactly as the captured cons tail used
1341    /// to be.
1342    Frame {
1343        vals: Box<[Value]>,
1344        home: Env,
1345    },
1346}
1347
1348/// A value unwinding through the single error channel (see the *errors* section
1349/// of `docs/elly-spec.md`). Evaluation returns `Result<Value, Raised>`; a raise
1350/// unwinds the `?` chain up to the nearest boundary. Two **kinds** unwind
1351/// together, differing only in what catches them:
1352///
1353/// - `Error(v)` — a genuine failure: a host abort (`raise_sym`) or an explicit
1354///   `__Err.raise v`. Propagates past `__match`.
1355/// - `NoMatch(v)` — a **pattern refutation** produced by `match_pattern` on a
1356///   structural miss. `v` is the *original* descriptive error the refutation
1357///   stands for (e.g. `.list_size_mismatch`, or `.no_match` for a bare `=`/`as`
1358///   miss). Only `__match` singles it out — catching it, discarding `v`, and
1359///   falling through to its fallback. Every **other** boundary (`__Err.catch`,
1360///   the top level) treats it exactly as `Error(v)`, so an *uncaught* refutation
1361///   surfaces as its original error `v` rather than an opaque signal.
1362///
1363/// The kinds are distinguished by construction, not by an inspectable tag: user
1364/// code only ever raises `Error` (via `__Err.raise`), so it cannot forge a
1365/// `NoMatch` and fool `__match` (unlike a bare `.no_match` symbol would).
1366#[derive(Debug, Clone)]
1367pub enum Raised {
1368    /// A genuine error, propagated by `__match`.
1369    Error(Value),
1370    /// A pattern refutation carrying the original error it decays to when uncaught.
1371    NoMatch(Value),
1372}
1373
1374impl Raised {
1375    /// The carried payload, regardless of kind (an uncaught refutation *is* its
1376    /// original error).
1377    pub fn value(&self) -> &Value {
1378        match self {
1379            Raised::Error(v) | Raised::NoMatch(v) => v,
1380        }
1381    }
1382
1383    /// Consume into the carried payload.
1384    pub fn into_value(self) -> Value {
1385        match self {
1386            Raised::Error(v) | Raised::NoMatch(v) => v,
1387        }
1388    }
1389}
1390
1391/// Raise one of the host-failure symbol tags (stored without its leading dot) as
1392/// a genuine `Error`. The tag is a `'static` literal, so it becomes a
1393/// zero-cost borrowed [`Text`] via `from_static`.
1394fn raise_sym(tag: &'static str) -> Raised {
1395    Raised::Error(Value::Symbol(Text::from_static(tag)))
1396}
1397
1398/// The unit value: the empty list `[]`. Fed by the nullary marker `()` and used
1399/// to force the `__Int.*` / `__Err.catch` branch thunks.
1400fn unit() -> Value {
1401    Value::List(Vector::new())
1402}
1403
1404/// Evaluate an expression in the empty environment.
1405///
1406/// The [`Ctx`] threaded through evaluation carries the per-runtime **unique-id
1407/// source**: each `&`-abstraction stamps its closure with the next id, giving
1408/// callables an identity for equality/order.
1409pub fn eval(expr: &Expr) -> Result<Value, Raised> {
1410    let ctx = Ctx::new();
1411    eval_env(expr, &None, &ctx)
1412}
1413
1414/// Apply a value to a single argument — the public entry point behind a host's
1415/// "call this value" surface (e.g. the Python bindings' `elly.Fun.__call__`).
1416///
1417/// This is the same application step the evaluator uses internally (`apply_n`
1418/// with one argument): a closure call, list projection, or one step toward
1419/// saturating a builtin. Since values are `'static`-owned, `f` and `arg` may come
1420/// from different programs. A fresh [`Ctx`] is used, mirroring [`eval`], so any
1421/// closures minted while running the application are stamped from its id source.
1422/// Use [`apply_with`] to apply under a context whose ids continue an earlier
1423/// one's.
1424pub fn apply(f: &Value, arg: Value) -> Result<Value, Raised> {
1425    apply_with(f, arg, &Ctx::new())
1426}
1427
1428/// Apply a value to a single argument under a caller-supplied [`Ctx`]. Identical
1429/// to [`apply`] except that the context comes from the caller, so the identities
1430/// minted by the call continue that context's id source instead of restarting at
1431/// zero. A host that hands callables out and takes them back needs it (e.g.
1432/// `elly.Fun.__call__` on a closure materialized out of a module), or an item and
1433/// an unrelated closure could be handed the same id.
1434pub fn apply_with(f: &Value, arg: Value, ctx: &Ctx) -> Result<Value, Raised> {
1435    apply1(f.clone(), arg, ctx)
1436}
1437
1438/// Evaluate an expression in a given `env` with a `Ctx` (carrying the unique-id
1439/// source). The public entry behind a host's "eval with environment" surface
1440/// (e.g. `elly.Env.eval`); [`eval`] is a thin wrapper around it.
1441pub fn eval_env(expr: &Expr, env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
1442    match expr {
1443        // A resolved reference: walk `index` cons links and clone the cell's
1444        // value. The resolver (`resolve.rs`) guarantees the index is in range, so
1445        // this neither compares names nor walks past the target.
1446        Expr::Local { index, .. } => Ok(lookup(env, *index)),
1447        // Names are rewritten to `Expr::Local` by the resolver before eval, and
1448        // `elly::parse` always resolves, so a bare `Name` never reaches here.
1449        Expr::Name(n) => unreachable!("unresolved name {:?} reached eval", n),
1450        // A module sibling reference; outlined whole (see [`mod_item`]).
1451        Expr::ModItem { index, depth, .. } => mod_item(env, *index, *depth, ctx),
1452        // A home-module leaf; outlined for the same reason (see [`home_leaf`]).
1453        Expr::Home { leaf, depth } => home_leaf(env, *leaf, *depth),
1454        // A recursive binding group; outlined (see [`recur`]).
1455        Expr::Recur(r) => recur(r, env, ctx),
1456        // A parse-resolved builtin. A 0-arity builtin (`__Map.nil`) is a value: it
1457        // fires immediately with no arguments rather than sitting as an unsaturated
1458        // `Builtin`; any other arity yields a `Builtin` awaiting its arguments.
1459        Expr::Builtin(op) => {
1460            if op.arity() == 0 {
1461                invoke(*op, &[], ctx)
1462            } else {
1463                // `Vec::new()`, not `Vec::with_capacity(op.arity())`: reserving here
1464                // is a measured pessimization. This value is created empty and then
1465                // *cloned* by each curried application, and `Vec::clone` allocates
1466                // `capacity == len` — so the reserved buffer is dropped before the
1467                // first `push` ever happens, and the push re-allocates. Reserving
1468                // costs one extra malloc/free per builtin reference: +25% to +105%
1469                // allocations and +5% to +23% time across the eval suite.
1470                Ok(Value::Builtin {
1471                    op: *op,
1472                    args: Vec::new(),
1473                })
1474            }
1475        }
1476        Expr::Int(n) => Ok(Value::from_bigint_ref(n)),
1477        Expr::Symbol(s) => Ok(Value::Symbol(s.clone())),
1478        // Self-evaluating: clone the shared, already-decoded handle.
1479        Expr::Str(s) => Ok(Value::Str(s.clone())),
1480        Expr::List(elems) => {
1481            let mut vs = Vector::new();
1482            for e in elems {
1483                vs.push_back_mut(eval_env(e, env, ctx)?);
1484            }
1485            Ok(Value::List(vs))
1486        }
1487        // A map literal: evaluate each key and value (source order), inserting
1488        // into a fresh map. A later entry with an equal key overwrites.
1489        Expr::Map(entries) => {
1490            let mut m = HashTrieMap::new();
1491            for (k, v) in entries {
1492                let kv = eval_env(k, env, ctx)?;
1493                let vv = eval_env(v, env, ctx)?;
1494                m.insert_mut(kv, vv);
1495            }
1496            Ok(Value::Map(m))
1497        }
1498        // Build a closure over the shared `code`, capturing **only** the free
1499        // variables the body uses: the resolved capture plan says where to read each
1500        // one from in *this* environment and which frame slot it fills (see
1501        // `capture_env` and `EnvNode::Frame`). A closed abstraction captures nothing
1502        // and allocates no frame — it runs directly on the environment's terminal,
1503        // which is also how a module item's closure keeps its module alive. An
1504        // abstraction the resolver marked `Chain` — one the call it sits in consumes,
1505        // so it cannot outlive its environment — shares that environment outright.
1506        Expr::Abs(code) => {
1507            let env = match &code.captures {
1508                Captures::Chain => env.clone(),
1509                Captures::Frame(plan) => capture_env(env, plan),
1510                Captures::Unresolved => unreachable!("unresolved abstraction reached eval"),
1511            };
1512            Ok(Value::Closure {
1513                id: ctx.next_id(),
1514                code: Rc::clone(code),
1515                applied: 0,
1516                env,
1517            })
1518        }
1519        // Call-by-value over a flattened spine: evaluate the callee, then every
1520        // argument strictly left-to-right, then apply the whole call at once. The
1521        // argument values are gathered on the **stack** for the common small
1522        // arities (every builtin is ≤ 4, and Z-recursion is 1-argument-dominant),
1523        // so a call allocates no heap argument buffer; only a spine of five or
1524        // more arguments falls back to a `Vec`.
1525        Expr::App(items) => {
1526            let callee = eval_env(&items[0], env, ctx)?;
1527            let arg_exprs = &items[1..];
1528            match arg_exprs {
1529                [a] => apply1(callee, eval_env(a, env, ctx)?, ctx),
1530                [a, b] => {
1531                    let va = eval_env(a, env, ctx)?;
1532                    let vb = eval_env(b, env, ctx)?;
1533                    apply_n(callee, &[va, vb], ctx)
1534                }
1535                [a, b, c] => {
1536                    let va = eval_env(a, env, ctx)?;
1537                    let vb = eval_env(b, env, ctx)?;
1538                    let vc = eval_env(c, env, ctx)?;
1539                    apply_n(callee, &[va, vb, vc], ctx)
1540                }
1541                [a, b, c, d] => {
1542                    let va = eval_env(a, env, ctx)?;
1543                    let vb = eval_env(b, env, ctx)?;
1544                    let vc = eval_env(c, env, ctx)?;
1545                    let vd = eval_env(d, env, ctx)?;
1546                    apply_n(callee, &[va, vb, vc, vd], ctx)
1547                }
1548                _ => {
1549                    let mut argv: Vec<Value> = Vec::with_capacity(arg_exprs.len());
1550                    for a in arg_exprs {
1551                        argv.push(eval_env(a, env, ctx)?);
1552                    }
1553                    apply_n(callee, &argv, ctx)
1554                }
1555            }
1556        }
1557    }
1558}
1559
1560/// Apply a value to a single argument — the tight one-argument application path,
1561/// used by the single-argument call spine (the dominant shape), the fold builtins
1562/// (`__Int.for`, `__Map.*`), `__eq`, `__Err.catch`, and `__match` clauses. One
1563/// argument can never *over-*apply (a saturating call leaves nothing left over),
1564/// so this needs no loop or slice bookkeeping: it either binds the one remaining
1565/// parameter and runs the body, produces a partial closure (still more parameters
1566/// to come), gathers one argument onto a builtin, or projects a list.
1567fn apply1(fv: Value, av: Value, ctx: &Ctx) -> Result<Value, Raised> {
1568    match fv {
1569        Value::Closure {
1570            code, applied, env, ..
1571        } => {
1572            let start = applied as usize;
1573            // Match the one argument against the next parameter, consing its
1574            // bindings onto the closure's (captured or partially-applied) env.
1575            let call_env = match_pattern(&code.head[start], &av, env, ctx)?;
1576            if start + 1 < code.head.len() {
1577                // Still parameters to come: a fresh-id partial closure carrying
1578                // the consed prefix as its env (shared with siblings by refcount).
1579                return Ok(Value::Closure {
1580                    id: ctx.next_id(),
1581                    code,
1582                    applied: applied + 1,
1583                    env: call_env,
1584                });
1585            }
1586            eval_env(&code.body, &call_env, ctx)
1587        }
1588        Value::Builtin {
1589            op,
1590            args: mut gathered,
1591        } => {
1592            // A fresh unary builtin saturates from the stack — no `Vec`.
1593            if gathered.is_empty() && op.arity() == 1 {
1594                return invoke(op, &[av], ctx);
1595            }
1596            gathered.push(av);
1597            if gathered.len() == op.arity() {
1598                invoke(op, &gathered, ctx)
1599            } else {
1600                Ok(Value::Builtin { op, args: gathered })
1601            }
1602        }
1603        // A host callback gathers exactly as a builtin does; only the dispatch at
1604        // the end differs (an `Rc`'d closure rather than the `invoke` match).
1605        Value::HostFn {
1606            code,
1607            args: gathered,
1608        } => {
1609            if gathered.is_empty() && code.arity == 1 {
1610                return (code.f)(&[av], ctx);
1611            }
1612            let gathered = extended(&gathered, &[av]);
1613            if gathered.len() == code.arity {
1614                (code.f)(&gathered, ctx)
1615            } else {
1616                Ok(Value::HostFn {
1617                    code,
1618                    args: gathered,
1619                })
1620            }
1621        }
1622        // A module applied to a symbol materializes that item; a non-symbol
1623        // argument is not applicable (v0 has no `x arg` module call form).
1624        Value::Module(home) => match av {
1625            Value::Symbol(s) => materialize(&home, &s, ctx),
1626            _ => Err(raise_sym("not_applicable")),
1627        },
1628        // Everything else applied to a symbol **dispatches** through its
1629        // prototype (see [`receiver`]) — an object through its own, every other
1630        // kind through the builtin module for it.
1631        fv => receiver(fv, &av, &[], ctx),
1632    }
1633}
1634
1635/// Apply a value to a whole call's arguments at once — the single runtime
1636/// application entry. Covers closures (a multi-parameter match via the `applied`
1637/// counter), builtins (gathered until saturation), and the list-projection
1638/// fallback; **over-application loops onto the result**, so a flat spine behaves
1639/// exactly like the curried applications it stands for.
1640///
1641/// Evaluation is strict and fully specified: the caller has already evaluated
1642/// every argument left-to-right (see [`eval_env`]'s `App` arm), and this then
1643/// *matches* the patterns left-to-right. An argument-evaluation error therefore
1644/// surfaces before any pattern refutation — the intentional, documented order
1645/// (see `docs/done/2026-08-02_perf-less-currying.md` §3).
1646fn apply_n(mut fv: Value, mut args: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
1647    while !args.is_empty() {
1648        match fv {
1649            // Applying an abstraction *matches* the next arguments against its
1650            // remaining patterns (`code.head[applied..]`), left-to-right. Binding
1651            // the whole call at once: if the arguments run out first it is a
1652            // partial (a fresh-id closure with the bound prefix in `env`); once the
1653            // head saturates, the body runs in the extended environment and any
1654            // leftover arguments loop onto its result. A pattern miss raises
1655            // `NoMatch` (caught by `__match`, else decaying to the refutation tag).
1656            Value::Closure {
1657                code, applied, env, ..
1658            } => {
1659                let start = applied as usize;
1660                let remaining = code.head.len() - start;
1661                let take = remaining.min(args.len());
1662                // Thread the env through each parameter match, consing bindings
1663                // onto the closure's captured (or partially-applied) env.
1664                let mut call_env = env;
1665                for (pat, arg) in code.head[start..start + take].iter().zip(&args[..take]) {
1666                    call_env = match_pattern(pat, arg, call_env, ctx)?;
1667                }
1668                if take < remaining {
1669                    // Partial: no body runs yet; mint a fresh identity.
1670                    return Ok(Value::Closure {
1671                        id: ctx.next_id(),
1672                        code,
1673                        applied: applied + take as u8,
1674                        env: call_env,
1675                    });
1676                }
1677                fv = eval_env(&code.body, &call_env, ctx)?;
1678                args = &args[take..];
1679            }
1680            // Gather arguments toward the op's arity; invoke once saturated and
1681            // loop any leftover onto the result. When the incoming spine already
1682            // supplies the whole arity (the common case — a builtin freshly
1683            // referenced then applied), invoke **straight from the stack slice**,
1684            // building no `Value::Builtin { args: Vec }` at all.
1685            Value::Builtin {
1686                op,
1687                args: mut gathered,
1688            } => {
1689                let arity = op.arity();
1690                if gathered.is_empty() && args.len() >= arity {
1691                    fv = invoke(op, &args[..arity], ctx)?;
1692                    args = &args[arity..];
1693                } else {
1694                    let take = (arity - gathered.len()).min(args.len());
1695                    gathered.extend(args[..take].iter().cloned());
1696                    if gathered.len() < arity {
1697                        return Ok(Value::Builtin { op, args: gathered });
1698                    }
1699                    fv = invoke(op, &gathered, ctx)?;
1700                    args = &args[take..];
1701                }
1702            }
1703            // Same gather-and-fire loop as the builtin arm above, dispatching
1704            // through the callback the value carries.
1705            Value::HostFn {
1706                code,
1707                args: gathered,
1708            } => {
1709                let arity = code.arity;
1710                if gathered.is_empty() && args.len() >= arity {
1711                    fv = (code.f)(&args[..arity], ctx)?;
1712                    args = &args[arity..];
1713                } else {
1714                    let take = (arity - gathered.len()).min(args.len());
1715                    let gathered = extended(&gathered, &args[..take]);
1716                    if gathered.len() < arity {
1717                        return Ok(Value::HostFn {
1718                            code,
1719                            args: gathered,
1720                        });
1721                    }
1722                    fv = (code.f)(&gathered, ctx)?;
1723                    args = &args[take..];
1724                }
1725            }
1726            // A module applied to a symbol materializes that item; any
1727            // leftover arguments loop onto the materialized value (e.g.
1728            // `M.factorial 5`).
1729            Value::Module(home) => {
1730                match &args[0] {
1731                    Value::Symbol(s) => fv = materialize(&home, s, ctx)?,
1732                    _ => return Err(raise_sym("not_applicable")),
1733                }
1734                args = &args[1..];
1735            }
1736            // Dispatch, taking the **whole** rest of the spine with it: the
1737            // receiver and the method's own arguments go to the method in one
1738            // call, so `x .m a b` binds them in one pass and never builds the
1739            // partial `x.m` would be (see [`receiver`]).
1740            fv => return receiver(fv, &args[0], &args[1..], ctx),
1741        }
1742    }
1743    Ok(fv)
1744}
1745
1746/// `prefix` followed by `more`, in one allocation — how a [`Value::HostFn`]
1747/// gathers the next curried argument onto the ones it already holds. Sized up
1748/// front, so a partial application costs exactly one `malloc` and no realloc.
1749fn extended(prefix: &[Value], more: &[Value]) -> Box<[Value]> {
1750    let mut all = Vec::with_capacity(prefix.len() + more.len());
1751    all.extend_from_slice(prefix);
1752    all.extend_from_slice(more);
1753    all.into_boxed_slice()
1754}
1755
1756/// Run a fully-applied builtin over its arguments. Borrows the argument slice
1757/// (every arm reads by reference or clones what it keeps), so a spine that
1758/// saturates a builtin exactly can invoke straight from the caller's stack
1759/// arguments — no `Value::Builtin { args: Vec }` need be built or torn down.
1760fn invoke(op: Builtin, args: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
1761    match op {
1762        Builtin::IntAdd => int_arith(&args[0], &args[1], i64::checked_add, |a, b| a + b),
1763        Builtin::IntSub => int_arith(&args[0], &args[1], i64::checked_sub, |a, b| a - b),
1764        Builtin::IntMul => int_arith(&args[0], &args[1], i64::checked_mul, |a, b| a * b),
1765        // `x ^ y`, `y >= 0`. A negative exponent has no integer value.
1766        Builtin::IntPow => {
1767            let e =
1768                u32::try_from(&*as_big(&args[1])?).map_err(|_| raise_sym("negative_exponent"))?;
1769            // Small-base fast path: `i64::checked_pow`, promoting on overflow.
1770            if let Value::I64(x) = &args[0] {
1771                if let Some(z) = x.checked_pow(e) {
1772                    return Ok(Value::I64(z));
1773                }
1774            }
1775            Ok(Value::from_bigint(as_big(&args[0])?.pow(e)))
1776        }
1777        Builtin::IntDivrem => {
1778            // Fast path: both operands fit `i64`. `i64::MIN / -1` overflows, so on a
1779            // `None` from either `checked_*` we fall through to the `BigInt` path.
1780            if let (Value::I64(x), Value::I64(y)) = (&args[0], &args[1]) {
1781                if *y == 0 {
1782                    return Err(raise_sym("div_by_zero"));
1783                }
1784                if let (Some(q), Some(r)) = (x.checked_div_euclid(*y), x.checked_rem_euclid(*y)) {
1785                    let mut t = Vector::new();
1786                    t.push_back_mut(Value::I64(q));
1787                    t.push_back_mut(Value::I64(r));
1788                    return Ok(Value::List(t));
1789                }
1790            }
1791            let (q, r) = div_rem_euclid(&*as_big(&args[0])?, &*as_big(&args[1])?)?;
1792            let mut t = Vector::new();
1793            t.push_back_mut(Value::from_bigint(q));
1794            t.push_back_mut(Value::from_bigint(r));
1795            Ok(Value::List(t))
1796        }
1797        // ascending fold over `[from, to)`, index first: `onEach i acc`.
1798        // `from >= to` runs zero iterations and returns the initial state.
1799        Builtin::IntFor => {
1800            let f = &args[3];
1801            let mut acc = args[2].clone();
1802            // Fast path: both bounds fit `i64` — an inline counter, no per-iteration
1803            // `BigInt` allocation. (`i` only ever reaches `to <= i64::MAX`, so the
1804            // `i += 1` after the last iteration cannot overflow.)
1805            if let (Value::I64(from), Value::I64(to)) = (&args[0], &args[1]) {
1806                let (from, to) = (*from, *to);
1807                let mut i = from;
1808                while i < to {
1809                    let with_i = apply1(f.clone(), Value::I64(i), ctx)?;
1810                    acc = apply1(with_i, acc, ctx)?;
1811                    i += 1;
1812                }
1813                return Ok(acc);
1814            }
1815            // General path: a bound is a wide `BigInt` (an astronomically long loop).
1816            let to = as_big(&args[1])?.into_owned();
1817            let mut i = as_big(&args[0])?.into_owned();
1818            while i < to {
1819                let with_i = apply1(f.clone(), Value::from_bigint(i.clone()), ctx)?;
1820                acc = apply1(with_i, acc, ctx)?;
1821                i += 1;
1822            }
1823            Ok(acc)
1824        }
1825        // `onEqual () if a == b else onDiff ()` — structural for data, by identity
1826        // for callables (see `impl PartialEq for Value`). Nullary-thunk branches, so only
1827        // the taken side runs; the operands may be any values and never raise.
1828        Builtin::Eq => {
1829            let taken = if args[0] == args[1] {
1830                &args[2]
1831            } else {
1832                &args[3]
1833            };
1834            apply1(taken.clone(), args[0].clone(), ctx)
1835        }
1836        // Raise the (already-evaluated) argument as a genuine error, unwinding to
1837        // the nearest catch.
1838        Builtin::ErrRaise => Err(Raised::Error(args[0].clone())),
1839        // Force the wrapped thunk; on *any* raise, hand the payload to the handler
1840        // — `__Err.catch` is the universal boundary, so it catches a refutation
1841        // too, delivering the original error it decayed to. A raise from the
1842        // handler itself propagates (not re-caught here).
1843        Builtin::ErrCatch => match apply1(args[1].clone(), unit(), ctx) {
1844            Ok(v) => Ok(v),
1845            Err(r) => apply1(args[0].clone(), r.into_value(), ctx),
1846        },
1847        // The refutable form: `__match clauses val`. Try each clause (a matcher,
1848        // typically `&(<pat>) <body>`) against `val` in order; the first whose
1849        // pattern *matches* returns its body's value, and no later clause runs. A
1850        // clause that *refutes* (a `NoMatch`) is skipped and the next is tried;
1851        // every other raise — a real `Error` from the body or a `= <expr>`
1852        // sub-expression — propagates. Falling off the end (an empty list, or all
1853        // clauses refuting) itself refutes `.no_match`, so an uncaught `__match`
1854        // surfaces `.no_match` while a nested one can still be caught by an outer
1855        // matcher. `__match` is the *only* boundary
1856        // that singles out `NoMatch` (which user code cannot forge), making "only
1857        // pattern mismatches fall through" exact. See `docs/elly-spec.md` § patterns.
1858        Builtin::Match => {
1859            for clause in list(&args[0])?.iter() {
1860                match apply1(clause.clone(), args[1].clone(), ctx) {
1861                    Ok(v) => return Ok(v),
1862                    Err(Raised::NoMatch(_)) => continue,
1863                    Err(e) => return Err(e),
1864                }
1865            }
1866            Err(Raised::NoMatch(Value::Symbol(Text::from_static(
1867                "no_match",
1868            ))))
1869        }
1870        // Element count of a list.
1871        Builtin::ListSize => Ok(Value::I64(list(&args[0])?.len() as i64)),
1872        // Element at a 0-based integer index; out of range (or negative) raises
1873        // `.projection_out_of_range`, generalizing `.N` projection to a computed
1874        // index.
1875        Builtin::ListGet => {
1876            let t = list(&args[0])?;
1877            index_of(&args[1])?
1878                .and_then(|idx| t.get(idx))
1879                .cloned()
1880                .ok_or_else(|| raise_sym("projection_out_of_range"))
1881        }
1882        // Concatenate two lists: `[a…] ++ [b…]`.
1883        Builtin::ListAppend => {
1884            let mut r = list(&args[0])?.clone();
1885            for e in list(&args[1])?.iter() {
1886                r.push_back_mut(e.clone());
1887            }
1888            Ok(Value::List(r))
1889        }
1890        // Replace the element at index `i` with `v`, returning a new list;
1891        // out of range (or a negative index) raises `.projection_out_of_range`.
1892        Builtin::ListWith => {
1893            let t = list(&args[0])?;
1894            let v = args[2].clone();
1895            index_of(&args[1])?
1896                .and_then(|idx| t.set(idx, v))
1897                .map(Value::List)
1898                .ok_or_else(|| raise_sym("projection_out_of_range"))
1899        }
1900        // Split a list at position `n` into `[left, right]` (left holds the
1901        // first `n` elements). `n` must be in `[0, size]`, else
1902        // `.projection_out_of_range`.
1903        Builtin::ListSplit => {
1904            let t = list(&args[1])?;
1905            let n = index_of(&args[0])?
1906                .filter(|&n| n <= t.len())
1907                .ok_or_else(|| raise_sym("projection_out_of_range"))?;
1908            let mut left = Vector::new();
1909            let mut right = Vector::new();
1910            for (idx, e) in t.iter().enumerate() {
1911                if idx < n {
1912                    left.push_back_mut(e.clone());
1913                } else {
1914                    right.push_back_mut(e.clone());
1915                }
1916            }
1917            let mut pair = Vector::new();
1918            pair.push_back_mut(Value::List(left));
1919            pair.push_back_mut(Value::List(right));
1920            Ok(Value::List(pair))
1921        }
1922        // The empty map (a 0-arity value; `args` is empty).
1923        Builtin::MapNil => Ok(Value::Map(HashTrieMap::new())),
1924        // `m` with `k` bound to `v` (overwriting any prior binding). Any value is
1925        // a key — no key restriction, no bad-key error.
1926        Builtin::MapWith => {
1927            let m = map(&args[0])?;
1928            Ok(Value::Map(m.insert(args[1].clone(), args[2].clone())))
1929        }
1930        // Value at `k`; a missing key raises `.missing_key` (a catchable tag).
1931        Builtin::MapGet => {
1932            let m = map(&args[0])?;
1933            m.get(&args[1])
1934                .cloned()
1935                .ok_or_else(|| raise_sym("missing_key"))
1936        }
1937        // Parallel projection: a list of the values for a list of keys; any
1938        // missing key raises `.missing_key`.
1939        Builtin::MapGets => {
1940            let m = map(&args[0])?;
1941            let mut out = Vector::new();
1942            for k in list(&args[1])?.iter() {
1943                let v = m.get(k).cloned().ok_or_else(|| raise_sym("missing_key"))?;
1944                out.push_back_mut(v);
1945            }
1946            Ok(Value::List(out))
1947        }
1948        // Fold over the map (unspecified order): `onEach key value state -> state`.
1949        Builtin::MapFor => {
1950            let m = map(&args[0])?;
1951            let f = &args[2];
1952            let mut acc = args[1].clone();
1953            for (k, v) in m.iter() {
1954                let with_k = apply1(f.clone(), k.clone(), ctx)?;
1955                let with_v = apply1(with_k, v.clone(), ctx)?;
1956                acc = apply1(with_v, acc, ctx)?;
1957            }
1958            Ok(acc)
1959        }
1960        // Generic merge over the union of keys. For each key the callback gets the
1961        // key and each side's value as an option (`[]` none / `[v]` some) and
1962        // returns an option — `[]` drops the key, `[v]` sets it.
1963        Builtin::MapMerge => {
1964            let m1 = map(&args[0])?;
1965            let m2 = map(&args[1])?;
1966            let f = &args[2];
1967            let mut result = HashTrieMap::new();
1968            // Every key in the union, visited once (order unspecified): all of
1969            // `m1`'s keys, then `m2`'s keys that `m1` lacks.
1970            let union = m1.keys().chain(m2.keys().filter(|k| !m1.contains_key(*k)));
1971            for k in union {
1972                let k = k.clone();
1973                let with_k = apply1(f.clone(), k.clone(), ctx)?;
1974                let with_l = apply1(with_k, option(m1.get(&k)), ctx)?;
1975                let out = apply1(with_l, option(m2.get(&k)), ctx)?;
1976                if let Some(v) = un_option(&out)? {
1977                    result.insert_mut(k, v);
1978                }
1979            }
1980            Ok(Value::Map(result))
1981        }
1982        // Union with `b` winning on a key conflict (a `__Map.merge` special case).
1983        Builtin::MapCat => {
1984            let mut r = map(&args[0])?.clone();
1985            for (k, v) in map(&args[1])?.iter() {
1986                r.insert_mut(k.clone(), v.clone());
1987            }
1988            Ok(Value::Map(r))
1989        }
1990        // `m` with `k` removed (a no-op if absent); the persistent-update
1991        // counterpart of `__Map.with`.
1992        Builtin::MapWithout => {
1993            let m = map(&args[0])?;
1994            Ok(Value::Map(m.remove(&args[1])))
1995        }
1996        // Element count of a map, symmetric with `__List.size`.
1997        Builtin::MapSize => Ok(Value::I64(map(&args[0])?.size() as i64)),
1998        // Canonical signed decimal of an integer (`15` → `"15"`); the Int half of
1999        // the scalar→text bridge. A non-int raises `.not_an_int`.
2000        Builtin::IntStr => {
2001            let s = match &args[0] {
2002                Value::I64(x) => x.to_string(),
2003                Value::BigInt(n) => n.to_string(),
2004                _ => return Err(raise_sym("not_an_int")),
2005            };
2006            Ok(Value::Str(Text::from(s.as_str())))
2007        }
2008        // Build a string from a list of **codepoints** (Unicode scalar values). A
2009        // non-int element raises `.not_an_int`; a value outside `0..=0x10FFFF` or in
2010        // the surrogate range raises `.bad_codepoint`.
2011        Builtin::StrPack => {
2012            let t = list(&args[0])?;
2013            let mut s = String::new();
2014            for elem in t.iter() {
2015                let code = match elem {
2016                    Value::I64(x) => u32::try_from(*x).ok(),
2017                    Value::BigInt(n) => u32::try_from(&**n).ok(),
2018                    _ => return Err(raise_sym("not_an_int")),
2019                };
2020                let cp = code
2021                    .and_then(char::from_u32)
2022                    .ok_or_else(|| raise_sym("bad_codepoint"))?;
2023                s.push(cp);
2024            }
2025            Ok(Value::Str(Text::from(s.as_str())))
2026        }
2027        // Explode a string into a list of its codepoints — the inverse of
2028        // `__Str.pack`. A non-string raises `.not_a_str`.
2029        Builtin::StrUnpack => {
2030            let mut t = Vector::new();
2031            for ch in str_ref(&args[0])?.chars() {
2032                t.push_back_mut(Value::I64(ch as i64));
2033            }
2034            Ok(Value::List(t))
2035        }
2036        // Intern a string as a symbol (`"foo"` → `.foo`); the string→symbol half of
2037        // the text bridge. The text must be a single Muon `<sym>` segment — the
2038        // only form that re-parses from `.<text>` — else `.bad_symbol`. A
2039        // non-string raises `.not_a_str`.
2040        Builtin::SymFrom => {
2041            let s = str_ref(&args[0])?;
2042            if is_sym_segment(s) {
2043                Ok(Value::Symbol(s.clone()))
2044            } else {
2045                Err(raise_sym("bad_symbol"))
2046            }
2047        }
2048        // A symbol's text without the dot (`.foo` → `"foo"`); the Sym half of the
2049        // scalar→text bridge and the inverse of `__Sym.from`. A non-symbol raises
2050        // `.not_a_sym`.
2051        Builtin::SymStr => match &args[0] {
2052            Value::Symbol(s) => Ok(Value::Str(s.clone())),
2053            _ => Err(raise_sym("not_a_sym")),
2054        },
2055        // The canonical name a module's code came from (see [`ModuleData::name`]).
2056        Builtin::ModName => match &args[0] {
2057            Value::Module(node) => match node.as_ref() {
2058                EnvNode::Module { data, .. } => Ok(match data.name() {
2059                    Some(name) => Value::Str(name.clone()),
2060                    // The host never named this code, so there is no name to
2061                    // report and the unit says so.
2062                    None => unit(),
2063                }),
2064                _ => Err(raise_sym("not_a_module")),
2065            },
2066            _ => Err(raise_sym("not_a_module")),
2067        },
2068        // The prototype of any value at all (see [`proto_of`]).
2069        Builtin::Proto => Ok(proto_of(&args[0], ctx)),
2070        // `__new proto data` — an object of the prototype the leaf supplied. The
2071        // prototype slot takes a module and nothing else, which is what keeps a
2072        // prototype a const-time entity with a known item table.
2073        Builtin::ObjNew => {
2074            let Value::Module(proto) = &args[0] else {
2075                return Err(raise_sym("not_a_module"));
2076            };
2077            Ok(Value::Object(Rc::new(ObjectData {
2078                proto: Rc::clone(proto),
2079                data: args[1].clone(),
2080            })))
2081        }
2082        // `__value proto obj` — the payload, provided the object is of that
2083        // prototype. A non-object is `.not_an_object`; an object of another
2084        // prototype is `.foreign_object`, so a method cannot unwrap a value that
2085        // merely passed through it.
2086        Builtin::ObjValue => {
2087            let Value::Module(proto) = &args[0] else {
2088                return Err(raise_sym("not_a_module"));
2089            };
2090            let Value::Object(obj) = &args[1] else {
2091                return Err(raise_sym("not_an_object"));
2092            };
2093            if !Rc::ptr_eq(&obj.proto, proto) {
2094                return Err(raise_sym("foreign_object"));
2095            }
2096            Ok(obj.data.clone())
2097        }
2098        // A builtin module names itself: 0-arity, so a reference *is* the module
2099        // value (see [`Ctx::builtin_module`]).
2100        Builtin::Int
2101        | Builtin::Str
2102        | Builtin::Sym
2103        | Builtin::List
2104        | Builtin::Map
2105        | Builtin::Err
2106        | Builtin::Mod
2107        | Builtin::Fun => Ok(ctx.builtin_module(op)),
2108    }
2109}
2110
2111/// The **prototype** of any value: an object's own, and the builtin module for
2112/// every other kind — which is what makes [`__proto`](Builtin::Proto) total and
2113/// gives every value a nominal type.
2114///
2115/// An **iota**'s prototype is the module that declared it, the same answer an
2116/// object of that module gives: an iota is an identity a module minted, so `x as
2117/// Bool` holds for `Bool`'s iotas as it does for its objects. That is what lets a
2118/// type range over both.
2119///
2120/// The three **callable** kinds — a closure, a builtin, a host function — answer
2121/// with the one `__Fun`. They are one nominal type, the way two closures of
2122/// different arity are.
2123fn proto_of(v: &Value, ctx: &Ctx) -> Value {
2124    match v {
2125        Value::Object(obj) => obj.proto(),
2126        Value::Iota { home, .. } => Value::Module(Rc::clone(home)),
2127        Value::I64(_) | Value::BigInt(_) => ctx.builtin_module(Builtin::Int),
2128        Value::Symbol(_) => ctx.builtin_module(Builtin::Sym),
2129        Value::Str(_) => ctx.builtin_module(Builtin::Str),
2130        Value::List(_) => ctx.builtin_module(Builtin::List),
2131        Value::Map(_) => ctx.builtin_module(Builtin::Map),
2132        Value::Closure { .. } | Value::Builtin { .. } | Value::HostFn { .. } => {
2133            ctx.builtin_module(Builtin::Fun)
2134        }
2135        Value::Module(_) => ctx.builtin_module(Builtin::Mod),
2136    }
2137}
2138
2139/// Borrow either integer variant as a `BigInt`, or raise `.not_an_int`. A
2140/// [`Value::BigInt`] is borrowed *without* cloning; only the [`Value::I64`] case
2141/// materializes (an unavoidable small allocation, and only on the wide/mixed slow
2142/// path — the `i64` fast paths in `invoke` avoid it entirely).
2143fn as_big(v: &Value) -> Result<Cow<'_, BigInt>, Raised> {
2144    match v {
2145        Value::I64(x) => Ok(Cow::Owned(BigInt::from(*x))),
2146        Value::BigInt(n) => Ok(Cow::Borrowed(n)),
2147        _ => Err(raise_sym("not_an_int")),
2148    }
2149}
2150
2151/// Shared shape for the binary `+ - *` builtins: an `i64` fast path (the `checked`
2152/// op, promoting to `BigInt` on overflow) with a `BigInt` fallback for wide
2153/// operands. The fallback borrows its operands (reference arithmetic, so a wide
2154/// `a + b` allocates only the result — matching the pre-split cost) and normalizes
2155/// through [`Value::from_bigint`], preserving the canonical `I64`-if-it-fits invariant.
2156fn int_arith(
2157    a: &Value,
2158    b: &Value,
2159    small: impl Fn(i64, i64) -> Option<i64>,
2160    big: impl Fn(&BigInt, &BigInt) -> BigInt,
2161) -> Result<Value, Raised> {
2162    if let (Value::I64(x), Value::I64(y)) = (a, b) {
2163        if let Some(z) = small(*x, *y) {
2164            return Ok(Value::I64(z));
2165        }
2166    }
2167    Ok(Value::from_bigint(big(&*as_big(a)?, &*as_big(b)?)))
2168}
2169
2170/// Borrow a value as a list, or raise `.not_a_list` — the strict-argument
2171/// check shared by the `__List.*` builtins.
2172fn list(v: &Value) -> Result<&Vector<Value>, Raised> {
2173    match v {
2174        Value::List(t) => Ok(t),
2175        _ => Err(raise_sym("not_a_list")),
2176    }
2177}
2178
2179/// Borrow a value as a string, or raise `.not_a_str` — the strict-argument check
2180/// shared by the `__Str.*` / `__Sym.*` builtins.
2181fn str_ref(v: &Value) -> Result<&Text, Raised> {
2182    match v {
2183        Value::Str(s) => Ok(s),
2184        _ => Err(raise_sym("not_a_str")),
2185    }
2186}
2187
2188/// Is `s` a single Muon `<sym>` — a non-empty run of symchars (`[0-9A-Za-z_+-]`)?
2189/// This is exactly the text that `.<s>` lexes back as one symbol, so it is the
2190/// admissible domain of `__Sym.from`. Anything with a space, a `.`, a quote, or
2191/// any other non-symchar (or the empty string) is rejected.
2192fn is_sym_segment(s: &str) -> bool {
2193    !s.is_empty()
2194        && s.bytes()
2195            .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'_' | b'-' | b'+'))
2196}
2197
2198/// Borrow a value as a map, or raise `.not_a_map` — the strict-argument check
2199/// shared by the `__Map.*` builtins.
2200fn map(v: &Value) -> Result<&HashTrieMap<Value, Value>, Raised> {
2201    match v {
2202        Value::Map(m) => Ok(m),
2203        _ => Err(raise_sym("not_a_map")),
2204    }
2205}
2206
2207/// Encode an optional map value as `__Map.merge`'s option: `[]` (none) or `[v]`
2208/// (some). Fed to the merge callback for each side of a key.
2209fn option(v: Option<&Value>) -> Value {
2210    match v {
2211        None => unit(),
2212        Some(v) => {
2213            let mut t = Vector::new();
2214            t.push_back_mut(v.clone());
2215            Value::List(t)
2216        }
2217    }
2218}
2219
2220/// Decode a `__Map.merge` callback result as an option: `[]` → `None` (drop the
2221/// key), `[v]` → `Some(v)` (set it). A non-list raises `.not_a_list`; any other
2222/// arity raises `.list_size_mismatch`.
2223fn un_option(v: &Value) -> Result<Option<Value>, Raised> {
2224    let t = list(v)?;
2225    match t.len() {
2226        0 => Ok(None),
2227        1 => Ok(Some(t.get(0).unwrap().clone())),
2228        _ => Err(raise_sym("list_size_mismatch")),
2229    }
2230}
2231
2232/// Read an integer value as a 0-based index. A non-integer raises `.not_an_int`;
2233/// a negative or out-of-`usize` value yields `None` (the caller decides the
2234/// out-of-range error). Shared by `__List.get` / `__List.with` / `__List.split`.
2235fn index_of(v: &Value) -> Result<Option<usize>, Raised> {
2236    match v {
2237        // A negative `i64` fails `usize::try_from` → `None` (out of range).
2238        Value::I64(x) => Ok(usize::try_from(*x).ok()),
2239        Value::BigInt(n) => Ok(u64::try_from(&**n)
2240            .ok()
2241            .and_then(|u| usize::try_from(u).ok())),
2242        _ => Err(raise_sym("not_an_int")),
2243    }
2244}
2245
2246/// Euclidean quotient/remainder: `a = b*q + r` with `0 <= r < |b|`. `b == 0`
2247/// raises `.div_by_zero`.
2248fn div_rem_euclid(a: &BigInt, b: &BigInt) -> Result<(BigInt, BigInt), Raised> {
2249    if b.sign() == Sign::NoSign {
2250        return Err(raise_sym("div_by_zero"));
2251    }
2252    let q = a / b; // truncated toward zero
2253    let r = a - &q * b;
2254    // Truncated `r` carries the sign of `a`; nudge it into `[0, |b|)`.
2255    Ok(if r.sign() == Sign::Minus {
2256        if b.sign() == Sign::Plus {
2257            (q - 1, r + b)
2258        } else {
2259            (q + 1, r - b)
2260        }
2261    } else {
2262        (q, r)
2263    })
2264}
2265
2266/// Resolve a de Bruijn `index` to a clone of its value: walk cons links along
2267/// `next`, counting the index down; a walk that reaches the closure's [`Frame`]
2268/// with `rest` left selects `vals[rest]` — the enclosing scope, flattened to one
2269/// array index. The resolver guarantees the index lands on a live cell or a live
2270/// frame slot, so the other arms are unreachable in a resolved program.
2271///
2272/// A [`Module`](EnvNode::Module) terminal is passed **through, uncounted**, into
2273/// the module's frame: the node binds no name, so a module body's reference to an
2274/// import or a prelude value is an ordinary `Local` whose index simply runs off
2275/// the lexical cells and continues into the frame.
2276///
2277/// [`Frame`]: EnvNode::Frame
2278fn lookup(env: &Env, index: u32) -> Value {
2279    let mut cur = env;
2280    let mut rest = index as usize;
2281    loop {
2282        match cur.as_deref() {
2283            Some(EnvNode::Cons { val, next }) => {
2284                if rest == 0 {
2285                    return val.clone();
2286                }
2287                rest -= 1;
2288                cur = next;
2289            }
2290            Some(EnvNode::Frame { vals, .. }) => match vals.get(rest) {
2291                Some(val) => return val.clone(),
2292                None => unreachable!("resolved index walked past the captured frame"),
2293            },
2294            None => unreachable!("resolved index walked past the environment"),
2295            Some(EnvNode::Module { frame, .. }) => cur = frame,
2296        }
2297    }
2298}
2299
2300/// The environment a capturing closure runs on, built from its resolved `plan`.
2301/// Outlined from `eval_env`, which is instruction-cache resident: the deep-recursion
2302/// benches (`rec_deep`, `mod_rec_deep`) pay for growing it.
2303#[inline(never)]
2304fn capture_env(env: &Env, plan: &[Capture]) -> Env {
2305    match plan {
2306        [] => env_home(env),
2307        // A one-value frame *is* a `Cons` onto the terminal: index 0 is the capture
2308        // and the walk ends there either way. Reusing the node the environment
2309        // already has costs one allocation instead of two (node + values), and
2310        // retains exactly the one value — worth a case of its own because a single
2311        // capture is the common escaping closure (`&v (x x) v` in a Z-combinator
2312        // recursion). Two or more go through a frame: the values then share one
2313        // allocation and one walk, which conses cannot.
2314        [c] => {
2315            let (val, next) = lookup_and_home(env, c.outer);
2316            Some(Rc::new(EnvNode::Cons { val, next }))
2317        }
2318        _ => {
2319            let (vals, home) = build_frame_and_home(env, plan);
2320            Some(Rc::new(EnvNode::Frame { vals, home }))
2321        }
2322    }
2323}
2324
2325/// [`lookup`] and [`env_home`] over **one** walk, for the one-capture closure the
2326/// `Abs` arm builds: the value at `index`, and the terminal the chain ends in.
2327/// Resuming the terminal walk from the cell the index landed on rather than from
2328/// `env` gives the same node — a chain is linear, so every walk out of it passes
2329/// through that cell — while skipping the links already crossed.
2330///
2331/// **Unless the index walked through the terminal.** Once a `Local` can pass
2332/// through a [`Module`](EnvNode::Module) node into its frame, the cell the index
2333/// stopped on may lie *inside* that frame, and resuming from there yields the
2334/// frame's own terminal rather than the module's. So the walk remembers the first
2335/// `Module` it passes and answers with that: it is exactly what
2336/// [`env_home`] would have returned from `env`, without walking twice.
2337fn lookup_and_home(env: &Env, index: u32) -> (Value, Env) {
2338    let mut cur = env;
2339    let mut rest = index as usize;
2340    let mut passed: Option<&Env> = None;
2341    let val = loop {
2342        match cur.as_deref() {
2343            Some(EnvNode::Cons { val, next }) => {
2344                if rest == 0 {
2345                    break val.clone();
2346                }
2347                rest -= 1;
2348                cur = next;
2349            }
2350            Some(EnvNode::Frame { vals, .. }) => match vals.get(rest) {
2351                Some(val) => break val.clone(),
2352                None => unreachable!("resolved index walked past the captured frame"),
2353            },
2354            None => unreachable!("resolved index walked past the environment"),
2355            Some(EnvNode::Module { frame, .. }) => {
2356                if passed.is_none() {
2357                    passed = Some(cur);
2358                }
2359                cur = frame;
2360            }
2361        }
2362    };
2363    let home = match passed {
2364        Some(home) => home.clone(),
2365        None => env_home(cur),
2366    };
2367    (val, home)
2368}
2369
2370/// Build a closure's captured frame from its resolved `plan` (see [`Capture`]) in
2371/// **one** walk of the defining environment: the plan is sorted by `outer`, so the
2372/// walk only ever moves forward — it advances to each capture's cell in turn and
2373/// writes the value to that capture's `slot`. Once the walk reaches the enclosing
2374/// [`Frame`](EnvNode::Frame) it stops moving: every remaining capture reads that
2375/// frame by direct index (`outer - depth`), the common case for a name from
2376/// further out than the immediate activation.
2377///
2378/// The frame is filled out of order (slot order is fixed at a free variable's first
2379/// use, not by its index), so it starts as a cheap placeholder fill; the resolver
2380/// guarantees each slot is written exactly once.
2381///
2382/// The walk also yields the chain's terminal, which the caller needs for the
2383/// frame's `home`: it is taken from where the last capture left the walk, not from
2384/// `env`, so the links the plan already crossed are not crossed twice — except
2385/// when the walk passed a [`Module`](EnvNode::Module) node, which it remembers for
2386/// the same reason [`lookup_and_home`] does.
2387fn build_frame_and_home(env: &Env, plan: &[Capture]) -> (Box<[Value]>, Env) {
2388    let mut vals: Vec<Value> = alloc::vec![Value::I64(0); plan.len()];
2389    let mut cur = env;
2390    let mut depth = 0u32;
2391    let mut passed: Option<&Env> = None;
2392    for cap in plan {
2393        loop {
2394            match cur.as_deref() {
2395                Some(EnvNode::Cons { val, next }) => {
2396                    if depth == cap.outer {
2397                        vals[cap.slot as usize] = val.clone();
2398                        break;
2399                    }
2400                    depth += 1;
2401                    cur = next;
2402                }
2403                Some(EnvNode::Frame { vals: outer, .. }) => {
2404                    match outer.get((cap.outer - depth) as usize) {
2405                        Some(val) => vals[cap.slot as usize] = val.clone(),
2406                        None => unreachable!("capture index walked past the captured frame"),
2407                    }
2408                    break;
2409                }
2410                // The terminal binds no name, so it is crossed without advancing
2411                // `depth`: a capture of a module's frame value counts only the
2412                // lexical cells between it and the reference.
2413                Some(EnvNode::Module { frame, .. }) => {
2414                    if passed.is_none() {
2415                        passed = Some(cur);
2416                    }
2417                    cur = frame;
2418                }
2419                None => unreachable!("capture index walked past the environment"),
2420            }
2421        }
2422    }
2423    let home = match passed {
2424        Some(home) => home.clone(),
2425        None => env_home(cur),
2426    };
2427    (vals.into_boxed_slice(), home)
2428}
2429
2430/// The environment's **terminal**: the empty environment (`None`) or the
2431/// [`Module`](EnvNode::Module) node a chain ends in, past any consed bindings and any
2432/// captured frame. A closure built here carries it on as its frame's `home`, so a
2433/// module item's body reached from inside the closure still finds its module (and
2434/// finds the same node, so no environment is allocated to run it).
2435fn env_home(env: &Env) -> Env {
2436    let mut cur = env;
2437    loop {
2438        match cur.as_deref() {
2439            Some(EnvNode::Cons { next, .. }) => cur = next,
2440            Some(EnvNode::Frame { home, .. }) => return home.clone(),
2441            Some(EnvNode::Module { .. }) => return cur.clone(),
2442            None => return None,
2443        }
2444    }
2445}
2446
2447/// Cons one binding of `val` onto `env`.
2448fn bind(val: Value, env: Env) -> Env {
2449    Some(Rc::new(EnvNode::Cons { val, next: env }))
2450}
2451
2452/// Materialize the module item at `index`: evaluate its stored body against
2453/// `home` — the [`Module`](EnvNode::Module) terminal alone, no lexical bindings —
2454/// yielding a value, typically a closure that captures `home` and so keeps the
2455/// module alive. Shared by `ModItem` resolution and module-applied-to-symbol
2456/// dispatch; an index outside the table raises `.missing_property`.
2457///
2458/// An item **whose body is a literal abstraction** is stamped with the item's
2459/// reserved id (`id_base + index`, see [`instantiate`]) instead of the fresh one
2460/// the `Abs` arm minted, so two accesses of one item produce *equal* closures.
2461/// They are interchangeable in every other respect already — same `Rc<Lambda>`
2462/// out of the `ModuleData`, same terminal, `applied: 0` — so the stamp is the
2463/// whole of what makes `M.f == M.f` hold, retiring v0's
2464/// `Test.factorial != Test.factorial` wart.
2465///
2466/// The stamp is keyed on the body's *shape*, not on the result being a closure,
2467/// so an item that merely forwards a callable keeps that callable's identity:
2468/// `g = f` materializes to the very same value `f` does, and `M.g == M.f`. Only
2469/// an item that *is* a function definition claims the item's own identity.
2470/// Anything else — a data item, a closure nested inside one (`pair = [&x x]`) —
2471/// is returned untouched.
2472///
2473/// A module **sibling reference** ([`Expr::ModItem`]): walk out to the home
2474/// module carried at the base of the environment ([`EnvNode::Module`]) and
2475/// materialize the item at `index` (evaluate its stored body, then
2476/// `materialize_body`). The index was assigned at resolve against this very module's
2477/// sorted table, so the lookup is a slice index with no name compare, and it is
2478/// in range. Only module-item bodies contain `ModItem`, and they always run under
2479/// a `Module` home terminal, so the walk finds one — through a closure's captured
2480/// frame, which carries the terminal on as its `home` (see [`EnvNode::Frame`]).
2481/// Handing the body the terminal the walk stopped on, rather than allocating a
2482/// fresh `Module` node, gives it exactly the module's static environment with no
2483/// lexical bindings — the `Cons`es and frames are stripped by the walk — while
2484/// saving an allocation per reference.
2485///
2486/// Outlined **whole**, walk included, and worth being deliberate about:
2487/// [`eval_env`] is instruction-cache resident, and growing this arm in place cost
2488/// ~9% on `rec_deep` — a benchmark that loads no module at all — and ~17% on
2489/// `mod_rec_deep`. Leaving one call in the arm is both smaller than the walk it
2490/// replaces and faster than either inlined form.
2491///
2492/// A member index at or past the item table is an **iota** — see
2493/// [`ModuleData::iota_names`] — and needs no materialization at all: the value is
2494/// the terminal this walk just found plus the position, so the arm falls out of
2495/// the same walk, the same bounds compare, and one refcount bump.
2496///
2497/// `depth` is how many terminals to **step over** before the owning one: a
2498/// [`recur`](Expr::Recur) nested in a module body puts two on the chain, and the
2499/// module's own items have to skip the `recur`'s. A step crosses into the skipped
2500/// terminal's frame, which is where the enclosing module sits. It is `0` for every
2501/// reference in a program with no nesting, so the common path is the loop it was
2502/// before.
2503#[inline(never)]
2504fn mod_item(env: &Env, index: u32, depth: u32, ctx: &Ctx) -> Result<Value, Raised> {
2505    let mut cur = env;
2506    let mut depth = depth;
2507    let (data, id_base) = loop {
2508        match cur.as_deref() {
2509            Some(EnvNode::Cons { next, .. }) => cur = next,
2510            Some(EnvNode::Frame { home, .. }) => cur = home,
2511            Some(EnvNode::Module {
2512                data,
2513                id_base,
2514                frame,
2515            }) => {
2516                if depth == 0 {
2517                    break (&**data, *id_base);
2518                }
2519                depth -= 1;
2520                cur = frame;
2521            }
2522            None => return Err(raise_sym("no_module")),
2523        }
2524    };
2525    let Some(body) = data.item(index) else {
2526        // Past the item table: an iota, addressed by the same member index space.
2527        // Derived here rather than stored anywhere — the terminal the walk landed
2528        // on *is* the identity, so this is a bounds compare and a refcount bump.
2529        let index = index - data.len() as u32;
2530        if data.iota(index).is_none() {
2531            return Err(raise_sym("missing_property"));
2532        }
2533        let home = cur.clone().expect("the walk stopped on a module terminal");
2534        return Ok(Value::Iota { home, index });
2535    };
2536    materialize_body(body, cur, id_base, index, ctx)
2537}
2538
2539/// Walk out to the [`Module`](EnvNode::Module) terminal `depth` steps in and hand
2540/// back one of the three [home-module leaves](crate::HomeLeaf) over it.
2541///
2542/// The walk is [`mod_item`]'s without the table index: a leaf addresses the
2543/// terminal itself rather than a member of it. `depth` is how many terminals to
2544/// step over, and it is the resolver that decided which one to land on — a leaf
2545/// names the nearest terminal *written as a module*, and eval learns no kind, so
2546/// the count is all it needs.
2547///
2548/// `__new` and `__value` are the object builtins **pre-applied to the module**,
2549/// which is an ordinary partial application: what comes back takes the one
2550/// remaining argument through the existing currying path, with no evaluation
2551/// machinery of its own. Being values, they can be handed out — a module that
2552/// exports `mint = __new` has given away construction deliberately.
2553#[inline(never)]
2554fn home_leaf(env: &Env, leaf: HomeLeaf, depth: u32) -> Result<Value, Raised> {
2555    let mut cur = env;
2556    let mut depth = depth;
2557    let home = loop {
2558        match cur.as_deref() {
2559            Some(EnvNode::Cons { next, .. }) => cur = next,
2560            Some(EnvNode::Frame { home, .. }) => cur = home,
2561            Some(EnvNode::Module { frame, .. }) => {
2562                if depth == 0 {
2563                    break cur.clone().expect("the walk stopped on a module terminal");
2564                }
2565                depth -= 1;
2566                cur = frame;
2567            }
2568            None => return Err(raise_sym("no_module")),
2569        }
2570    };
2571    let op = match leaf {
2572        HomeLeaf::Module => return Ok(Value::Module(home)),
2573        HomeLeaf::New => Builtin::ObjNew,
2574        HomeLeaf::Value => Builtin::ObjValue,
2575    };
2576    Ok(Value::Builtin {
2577        op,
2578        args: alloc::vec![Value::Module(home)],
2579    })
2580}
2581
2582/// Evaluate a [`recur`](Expr::Recur): build a [`Module`](EnvNode::Module) terminal
2583/// over the **current environment** and run the body under it.
2584///
2585/// This is instantiation with a cons-shaped frame. Where [`instantiate`] copies a
2586/// fixed slot list into a flat [`Frame`](EnvNode::Frame), here the frame *is* the
2587/// enclosing lexical environment, shared by one refcount bump — so entering a
2588/// `recur` allocates exactly one node and copies nothing. A binding's reference to
2589/// an enclosing name is then the same de Bruijn index it would have had inline at
2590/// the `recur` site, because the terminal is not counted by the walk.
2591///
2592/// The id block is reserved **per evaluation**, so two activations of the same
2593/// `recur` mint different item identities — which is the wanted semantics: they
2594/// close over different frames and are not the same function.
2595///
2596/// Nothing is forced at entry. The bindings stay dehydrated in the table and are
2597/// materialized on reference, so a group may hold a body that would raise, or one
2598/// that only makes sense after another has run.
2599#[inline(never)]
2600fn recur(group: &Rc<ModuleData>, env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
2601    let home = Some(Rc::new(EnvNode::Module {
2602        id_base: ctx.reserve_ids(group.len() as u64),
2603        data: Rc::clone(group),
2604        frame: env.clone(),
2605    }));
2606    eval_env(recur_body(group), &home, ctx)
2607}
2608
2609/// Evaluate item `index`'s `body` in its module terminal `home` — the shared tail
2610/// of both materialization paths, the `ModItem` walk ([`mod_item`]) and the
2611/// by-name `M.name` access ([`materialize`]).
2612///
2613/// A **function item** — a body that is a literal abstraction, the overwhelmingly
2614/// common shape — is built here directly, taking the id reserved for it at
2615/// instantiation (`id_base + index`, see [`instantiate`]) rather than a fresh one.
2616/// Two accesses of one item then produce *equal* closures: they are
2617/// interchangeable in every other respect already — same `Rc<Lambda>` out of the
2618/// `ModuleData`, same terminal, `applied: 0` — so this is the whole of what
2619/// retires v0's `Test.factorial != Test.factorial` wart.
2620///
2621/// Building it here rather than stamping over what [`eval_env`]'s `Abs` arm
2622/// returns is what makes the fix free: it skips a dispatch through `eval_env`
2623/// *and* the `Ctx` counter write, paying for the terminal walk that precedes it.
2624/// Handing the value back out of `eval_env` to overwrite its `id` costs ~4% on
2625/// `mod_fib_naive` instead — taking `&mut` on the result forces it to memory.
2626///
2627/// The identity is keyed on the body's *shape*, not on the result being a
2628/// closure, so an item that merely forwards a callable keeps that callable's
2629/// identity: `g = f` materializes to the very same value `f` does, and
2630/// `M.g == M.f`. Only an item that *is* a function definition claims the item's
2631/// own identity. Anything else — a data item, a closure nested inside one
2632/// (`pair = [&x x]`) — goes through `eval_env` untouched.
2633#[inline]
2634fn materialize_body(
2635    body: &Expr,
2636    home: &Env,
2637    id_base: u64,
2638    index: u32,
2639    ctx: &Ctx,
2640) -> Result<Value, Raised> {
2641    if let Expr::Abs(code) = body {
2642        let env = match &code.captures {
2643            Captures::Chain => home.clone(),
2644            Captures::Frame(plan) => capture_env(home, plan),
2645            Captures::Unresolved => unreachable!("unresolved abstraction reached eval"),
2646        };
2647        return Ok(Value::Closure {
2648            id: id_base + index as u64,
2649            code: Rc::clone(code),
2650            applied: 0,
2651            env,
2652        });
2653    }
2654    eval_env(body, home, ctx)
2655}
2656
2657/// Materialize the item **named** `name` out of the module object whose terminal
2658/// is `home`: binary-search the name to its index, then evaluate its body in that
2659/// terminal and stamp it exactly as the `ModItem` arm of [`eval_env`] does for a
2660/// sibling reference. Reusing the object's own node — the object *is* the
2661/// terminal — means an `M.name` access allocates no environment (v0 built a fresh
2662/// `Module` node per access); it costs one refcount bump to make the `Env` the
2663/// evaluator takes. A name absent from the module raises `.missing_property`.
2664fn materialize(home: &Rc<EnvNode>, name: &Text, ctx: &Ctx) -> Result<Value, Raised> {
2665    let EnvNode::Module { data, id_base, .. } = &**home else {
2666        unreachable!("a module object always holds a module terminal")
2667    };
2668    let index = data
2669        .index_of(name)
2670        .ok_or_else(|| raise_sym("missing_property"))?;
2671    let body = data.item(index).expect("index_of returned a live index");
2672    materialize_body(body, &Some(Rc::clone(home)), *id_base, index, ctx)
2673}
2674
2675/// Apply a **receiver** to a symbol: the dispatch arm of `apply1` / `apply_n`,
2676/// covering every value kind that is neither callable nor a module.
2677///
2678/// The rule is one line — the symbol names a member of the receiver's prototype,
2679/// and the receiver is passed to it — and [`proto_of`] is what makes it total: an
2680/// object dispatches through its own prototype, and every other kind through the
2681/// builtin module for it, so `2 .add 3` reaches `__Int.add 2 3` through the
2682/// receiver rather than by being written out.
2683///
2684/// **List projection wins over dispatch**, for now. A numeric symbol on a list
2685/// projects as it always has; any other symbol dispatches to `__List` instead of
2686/// raising `.bad_projection`. The two symbol spaces are syntactically disjoint
2687/// (`.0` versus `.get`), so nothing shadows. The coexistence is temporary: `.N`
2688/// projection is a vestige of `( … )`-as-tuple, and a real projection syntax
2689/// (TODO.md) would leave the symbol space on a list to dispatch alone.
2690///
2691/// A non-symbol argument raises `.not_applicable` — there is no `x arg` call form
2692/// for a non-callable.
2693///
2694/// **Outlined**, like [`mod_item`] and for the same reason: it sits in the
2695/// fall-through arm of `apply1` and `apply_n`, which are as instruction-cache
2696/// resident as [`eval_env`] is. Letting it inline there cost ~17% on `rec_deep`
2697/// — a benchmark that dispatches on nothing at all — and nothing else moved (see
2698/// `docs/done/2026-08-13_elly-modules-env.md` on that measurement).
2699#[inline(never)]
2700fn receiver(this: Value, name: &Value, rest: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
2701    let Value::Symbol(s) = name else {
2702        return Err(raise_sym("not_applicable"));
2703    };
2704    if let Value::List(elems) = &this {
2705        if is_index(s) {
2706            let projected = project(elems, s)?;
2707            return apply_n(projected, rest, ctx);
2708        }
2709    }
2710    let proto = match &this {
2711        Value::Object(obj) => Rc::clone(&obj.proto),
2712        other => match proto_of(other, ctx) {
2713            Value::Module(home) => home,
2714            _ => unreachable!("a prototype is always a module value"),
2715        },
2716    };
2717    dispatch(&proto, this, s, rest, ctx)
2718}
2719
2720/// **Dispatch** a method: materialize `name` out of `proto` and apply it to the
2721/// receiver, followed by `rest` — the remaining arguments of the call spine.
2722///
2723/// ```text
2724/// x .m a b   ≡   (materialize (proto of x) .m) x a b
2725/// ```
2726///
2727/// So `x.m` is the method with `self` already bound, and the method's own
2728/// parameters follow. The lookup is the very same [`materialize`] a written
2729/// `M.name` runs — one binary search over the item table — which is why a
2730/// [`local`](ModuleData::local_names) is a private method for free: it is not in
2731/// that table, so `x.helper` and `M.helper` both answer `.missing_property`.
2732///
2733/// The whole spine goes in one call rather than binding the receiver and looping,
2734/// so a two-parameter method never builds the partial `x.m` would be. The small
2735/// arities gather on the stack, as [`eval_env`]'s call spine does; only a spine of
2736/// four or more arguments past the receiver falls back to a `Vec`.
2737///
2738/// A name absent from the prototype's item table raises `.missing_property`, and
2739/// an item that materializes to a non-callable falls through to ordinary
2740/// application — both mirroring module access.
2741fn dispatch(
2742    proto: &Rc<EnvNode>,
2743    this: Value,
2744    name: &Text,
2745    rest: &[Value],
2746    ctx: &Ctx,
2747) -> Result<Value, Raised> {
2748    let method = materialize(proto, name, ctx)?;
2749    match rest {
2750        [] => apply1(method, this, ctx),
2751        [a] => apply_n(method, &[this, a.clone()], ctx),
2752        [a, b] => apply_n(method, &[this, a.clone(), b.clone()], ctx),
2753        [a, b, c] => apply_n(method, &[this, a.clone(), b.clone(), c.clone()], ctx),
2754        _ => {
2755            let mut all: Vec<Value> = Vec::with_capacity(rest.len() + 1);
2756            all.push(this);
2757            all.extend_from_slice(rest);
2758            apply_n(method, &all, ctx)
2759        }
2760    }
2761}
2762
2763/// Whether `sym` (a symbol's text, stored without its dot) is a **projection**
2764/// index — all ASCII digits, so `.0` and `.12` are projections while `.get` and
2765/// `.size` are member names. The two spaces are disjoint by shape, which is what
2766/// lets a list carry both (see [`receiver`]).
2767fn is_index(sym: &str) -> bool {
2768    !sym.is_empty() && sym.bytes().all(|b| b.is_ascii_digit())
2769}
2770
2771/// The refutation `⟂` a pattern probe reaches on a structural miss: a `NoMatch`
2772/// carrying `tag` (stored without its dot) as the *original* error it stands for.
2773/// `__match` catches it and falls through; uncaught, it decays to `.tag`.
2774fn no_match(tag: &'static str) -> Raised {
2775    Raised::NoMatch(Value::Symbol(Text::from_static(tag)))
2776}
2777
2778/// Whether `value`'s prototype is the module `expr` names — the general form of an
2779/// `as <Proto>` pattern, where [`ProtoRef::Kind`] is the compiled one. A reference
2780/// that is not a module raises `.not_a_module`: it is a mistake about the pattern
2781/// rather than about the subject, so it is a real error and not a refutation.
2782///
2783/// Outlined for the reason [`receiver`] is: `match_pattern` is on every call's
2784/// path, and its arms pay for each other's code size.
2785#[inline(never)]
2786fn proto_is(expr: &Expr, value: &Value, env: &Env, ctx: &Ctx) -> Result<bool, Raised> {
2787    let expected = eval_env(expr, env, ctx)?;
2788    if !matches!(expected, Value::Module(_)) {
2789        return Err(raise_sym("not_a_module"));
2790    }
2791    Ok(proto_of(value, ctx) == expected)
2792}
2793
2794/// The payload of `value`, provided it is an object whose prototype is the home
2795/// module `depth` terminals out — the whole of what a [`Unwrap`](Pattern::Unwrap)
2796/// pattern tests before descending into the pattern it wraps. Refutes
2797/// `.not_an_object` / `.foreign_object`, the tags the `__value` *expression*
2798/// raises, since unwrapping a foreign object has no other sound reading.
2799#[inline(never)]
2800fn unwrapped(value: &Value, env: &Env, depth: u32) -> Result<Value, Raised> {
2801    let Value::Object(obj) = value else {
2802        return Err(no_match("not_an_object"));
2803    };
2804    let Value::Module(home) = home_leaf(env, HomeLeaf::Module, depth)? else {
2805        unreachable!("the module leaf always yields a module value")
2806    };
2807    if !Rc::ptr_eq(&obj.proto, &home) {
2808        return Err(no_match("foreign_object"));
2809    }
2810    Ok(obj.data.clone())
2811}
2812
2813/// Match `value` against `pat`, consing any bindings onto `env` and returning the
2814/// extended environment, or a `Raised` on a miss. Bindings cons in the exact order
2815/// the resolver assigned de Bruijn indices, so an embedded `= <expr>` / map key
2816/// resolves against the env built so far (including names bound earlier in the same
2817/// pattern, `&[a, = a]`). A structural mismatch is a `NoMatch(tag)` refutation
2818/// (reproducing the same tags the old eliminator desugaring raised: `.not_a_list`,
2819/// `.list_size_mismatch`, `.not_a_map`, `.missing_key`, `.map_size_mismatch`,
2820/// `.no_match`); a real `Error` from an embedded expression propagates.
2821/// `Bind`/`Discard` are irrefutable; every other shape may refute.
2822fn match_pattern(pat: &Pattern, value: &Value, env: Env, ctx: &Ctx) -> Result<Env, Raised> {
2823    match pat {
2824        // `_` — matches anything, binds nothing.
2825        Pattern::Discard => Ok(env),
2826        // `name` — cons the subject.
2827        Pattern::Bind(_) => Ok(bind(value.clone(), env)),
2828        // `name = pat` — cons the whole subject, then match the inner pattern
2829        // against the same value.
2830        Pattern::At(_, inner) => match_pattern(inner, value, bind(value.clone(), env), ctx),
2831        // `= <ref>` — match iff the subject equals the comparand's value. The
2832        // comparand is a name or literal (a closed pattern grammar); evaluating a
2833        // name reads the env and raises only if genuinely unbound (rejected at parse
2834        // now), a normal reference `Error` that propagates; a plain inequality
2835        // refutes with the bare `.no_match` tag.
2836        Pattern::Equal(expr) => {
2837            let expected = eval_env(expr, &env, ctx)?;
2838            if *value == expected {
2839                Ok(env)
2840            } else {
2841                Err(no_match("no_match"))
2842            }
2843        }
2844        // `< <ref>` / `> <ref>` — ordered comparison. Ordering is defined only
2845        // within a comparable kind (`Int` today; `Str` later), so unless the
2846        // subject and the bound are both of one such kind this refutes `.not_int`
2847        // rather than raising; an in-kind comparison that does not hold refutes
2848        // the plain `.no_match`.
2849        Pattern::Less(expr) => {
2850            let bound = eval_env(expr, &env, ctx)?;
2851            order_match(value, &bound, Ordering::Less)?;
2852            Ok(env)
2853        }
2854        Pattern::Greater(expr) => {
2855            let bound = eval_env(expr, &env, ctx)?;
2856            order_match(value, &bound, Ordering::Greater)?;
2857            Ok(env)
2858        }
2859        // `pat as <Proto>` — narrow by prototype, then match the inner pattern; a
2860        // value of another prototype refutes `.no_match`.
2861        //
2862        // The compiled form is a discriminant test and never raises. The general
2863        // form evaluates its reference — an atom, resolved before the inner
2864        // pattern — and requires a *module*: a non-module reference is a real
2865        // error rather than a refutation, since it is a mistake about the pattern
2866        // rather than about the subject.
2867        Pattern::Type(proto, inner) => {
2868            let matched = match proto {
2869                ProtoRef::Kind(ty) => ty_matches(*ty, value),
2870                ProtoRef::Ref(expr) => proto_is(expr, value, &env, ctx)?,
2871            };
2872            if matched {
2873                match_pattern(inner, value, env, ctx)
2874            } else {
2875                Err(no_match("no_match"))
2876            }
2877        }
2878        // `__value <pat>` — an object of the home module, unwrapped. The prototype
2879        // check is the one `__value` the expression performs, and refutes with the
2880        // same two tags; the payload then goes to the inner pattern. Outlined, so
2881        // the arm costs the matcher one call (see [`unwrapped`]).
2882        Pattern::Unwrap { depth, inner } => {
2883            let data = unwrapped(value, &env, *depth)?;
2884            match_pattern(inner, &data, env, ctx)
2885        }
2886        // `(p1 | p2)` — committed: the first arm whose pattern matches wins (a
2887        // later failure in the body does not backtrack). A refuting left arm falls
2888        // through to the right, matched against the *original* env; both arms cons
2889        // the same binding sequence (checked by the resolver), so a reference after
2890        // the or-pattern reads one fixed index whichever arm matched. A real error
2891        // propagates.
2892        Pattern::Or(left, right) => match match_pattern(left, value, env.clone(), ctx) {
2893            Ok(e) => Ok(e),
2894            Err(Raised::NoMatch(_)) => match_pattern(right, value, env, ctx),
2895            Err(e) => Err(e),
2896        },
2897        Pattern::List(elems, rest) => match_list(elems, rest, value, env, ctx),
2898        Pattern::Map(entries, rest) => match_map(entries, rest, value, env, ctx),
2899    }
2900}
2901
2902/// Match a list pattern: kind (`.not_a_list`), then arity (`.list_size_mismatch`
2903/// — exact without a rest, a minimum with one), then the fixed prefix element by
2904/// element, binding a named rest to the remainder. The rest is bound *before* the
2905/// elements (so an element name shadows it), reproducing the old desugaring's
2906/// scope order.
2907fn match_list(
2908    elems: &[Pattern],
2909    rest: &Rest,
2910    value: &Value,
2911    env: Env,
2912    ctx: &Ctx,
2913) -> Result<Env, Raised> {
2914    let t = match value {
2915        Value::List(t) => t,
2916        _ => return Err(no_match("not_a_list")),
2917    };
2918    let n = elems.len();
2919    let has_rest = !matches!(rest, Rest::None);
2920    if (has_rest && t.len() < n) || (!has_rest && t.len() != n) {
2921        return Err(no_match("list_size_mismatch"));
2922    }
2923    let mut env = env;
2924    if let Rest::Named(_) = rest {
2925        let mut rem = Vector::new();
2926        for e in t.iter().skip(n) {
2927            rem.push_back_mut(e.clone());
2928        }
2929        env = bind(Value::List(rem), env);
2930    }
2931    for (i, p) in elems.iter().enumerate() {
2932        env = match_pattern(p, t.get(i).unwrap(), env, ctx)?;
2933    }
2934    Ok(env)
2935}
2936
2937/// Match a map pattern in two passes (see
2938/// `docs/done/2026-07-26_elly-patterns-native.md`): concrete-key **lookups**
2939/// claim their keys first, then **captures** pop the smallest remaining entry
2940/// each, then closedness / a named rest apply to what is left. Lookup key
2941/// expressions all evaluate **once**, in source order, before the kind test —
2942/// matching the old desugaring, whose key temps were bound outermost.
2943fn match_map(
2944    entries: &[(MapKey, Pattern)],
2945    rest: &Rest,
2946    value: &Value,
2947    env: Env,
2948    ctx: &Ctx,
2949) -> Result<Env, Raised> {
2950    // Evaluate every lookup key first (source order, against the incoming env —
2951    // before any of this pattern's binds); partition into lookups and captures. A
2952    // lookup key is a name or literal, so it raises only on an unbound name (a
2953    // normal reference error), which propagates here.
2954    let mut lookups: Vec<(Value, &Pattern)> = Vec::new();
2955    let mut captures: Vec<(&Pattern, &Pattern)> = Vec::new();
2956    for (key, vpat) in entries {
2957        match key {
2958            MapKey::Lookup(kexpr) => lookups.push((eval_env(kexpr, &env, ctx)?, vpat)),
2959            MapKey::Capture(kpat) => captures.push((kpat, vpat)),
2960        }
2961    }
2962    // Kind test after the keys evaluate.
2963    let m = match value {
2964        Value::Map(m) => m,
2965        _ => return Err(no_match("not_a_map")),
2966    };
2967    let mut env = env;
2968    // Pass 1: probe each concrete key, matching its value pattern and claiming
2969    // the key from the remainder. A missing key refutes `.missing_key`.
2970    let mut remainder = m.clone();
2971    for (k, vpat) in &lookups {
2972        match m.get(k) {
2973            Some(v) => {
2974                env = match_pattern(vpat, v, env, ctx)?;
2975                remainder = remainder.remove(k);
2976            }
2977            None => return Err(no_match("missing_key")),
2978        }
2979    }
2980    // Pass 2: each capture pops an unspecified remaining entry (the map is
2981    // unordered; which entry a capture peels is not a language guarantee),
2982    // matching the key and value patterns; an empty remainder refutes `.missing_key`.
2983    for (kpat, vpat) in &captures {
2984        // Clone the entry out first so the iterator borrow ends before the remove.
2985        let entry = remainder.iter().next().map(|(k, v)| (k.clone(), v.clone()));
2986        match entry {
2987            Some((k, v)) => {
2988                remainder = remainder.remove(&k);
2989                env = match_pattern(kpat, &k, env, ctx)?;
2990                env = match_pattern(vpat, &v, env, ctx)?;
2991            }
2992            None => return Err(no_match("missing_key")),
2993        }
2994    }
2995    // Tail: closed → the leftover must be empty; named rest → bind it; anon → drop.
2996    match rest {
2997        Rest::None => {
2998            if remainder.size() != 0 {
2999                return Err(no_match("map_size_mismatch"));
3000            }
3001        }
3002        Rest::Named(_) => env = bind(Value::Map(remainder), env),
3003        Rest::Anon => {}
3004    }
3005    Ok(env)
3006}
3007
3008/// Back the `< expr` / `> expr` ordering patterns: match iff `subject` compares
3009/// to `bound` as `want` (`Less` / `Greater`). Ordering is defined only within a
3010/// comparable kind — `Int` for now (later `Str` will join, lexicographically) —
3011/// so unless both operands share such a kind this refutes `.not_int`; an in-kind
3012/// comparison that does not hold refutes the plain `.no_match`.
3013fn order_match(subject: &Value, bound: &Value, want: Ordering) -> Result<(), Raised> {
3014    let cmp = match (subject, bound) {
3015        (Value::I64(a), Value::I64(b)) => a.cmp(b),
3016        (Value::BigInt(a), Value::BigInt(b)) => a.cmp(b),
3017        // Mixed magnitudes: by the canonical invariant a `BigInt` is `|n| > i64::MAX`
3018        // and never zero, so its sign alone orders it against any `i64`.
3019        (Value::I64(_), Value::BigInt(b)) => {
3020            if b.sign() == Sign::Minus {
3021                Ordering::Greater
3022            } else {
3023                Ordering::Less
3024            }
3025        }
3026        (Value::BigInt(a), Value::I64(_)) => {
3027            if a.sign() == Sign::Minus {
3028                Ordering::Less
3029            } else {
3030                Ordering::Greater
3031            }
3032        }
3033        (Value::Str(a), Value::Str(b)) => a.cmp(b),
3034        // Off-kind (not a shared `Int`/`Str` ordering): the refutation tag follows
3035        // the **comparand** (the bound demands the subject's kind). A `Str` bound
3036        // refutes `.not_string`; every other bound keeps the Int-only default
3037        // `.not_int`.
3038        _ => {
3039            let tag = if matches!(bound, Value::Str(_)) {
3040                "not_string"
3041            } else {
3042                "not_int"
3043            };
3044            return Err(no_match(tag));
3045        }
3046    };
3047    if cmp == want {
3048        Ok(())
3049    } else {
3050        Err(no_match("no_match"))
3051    }
3052}
3053
3054/// Whether `value` is of the value kind `ty` — the total, non-raising test behind
3055/// an `as <Proto>` pattern whose reference resolved to a builtin module
3056/// ([`ProtoRef::Kind`]). It is the compiled form of "`__proto value` is that
3057/// module", so it answers exactly what [`proto_of`] would, without the `Ctx` round
3058/// trip: `Fun` is any callable, `Mod` a module value, and an *object* is of none
3059/// of them — its prototype is its own.
3060fn ty_matches(ty: TyKind, value: &Value) -> bool {
3061    match ty {
3062        TyKind::Int => matches!(value, Value::I64(_) | Value::BigInt(_)),
3063        TyKind::Sym => matches!(value, Value::Symbol(_)),
3064        TyKind::Str => matches!(value, Value::Str(_)),
3065        TyKind::List => matches!(value, Value::List(_)),
3066        TyKind::Map => matches!(value, Value::Map(_)),
3067        TyKind::Fun => matches!(
3068            value,
3069            Value::Closure { .. } | Value::Builtin { .. } | Value::HostFn { .. }
3070        ),
3071        TyKind::Mod => matches!(value, Value::Module(_)),
3072    }
3073}
3074
3075/// Project position `sym` (an `N` symbol, stored without its leading dot) out of
3076/// a list's elements.
3077fn project(elems: &Vector<Value>, sym: &str) -> Result<Value, Raised> {
3078    let idx: usize = sym.parse().map_err(|_| raise_sym("bad_projection"))?;
3079    elems
3080        .get(idx)
3081        .cloned()
3082        .ok_or_else(|| raise_sym("projection_out_of_range"))
3083}
3084
3085impl Value {
3086    /// Build an integer value from a `BigInt`, **demoting** to the inline
3087    /// [`Value::I64`] form whenever it fits. This is the single choke point that
3088    /// maintains the canonical invariant — an integer is [`Value::BigInt`] *iff* it
3089    /// does not fit `i64` — on which `PartialEq`, `Hash`, and ordering all rely, so
3090    /// every wide arithmetic result must pass through here. Also the constructor
3091    /// hosts (the Python bindings) use to admit an arbitrary `BigInt`.
3092    pub fn from_bigint(n: BigInt) -> Value {
3093        match i64::try_from(&n) {
3094            Ok(i) => Value::I64(i),
3095            Err(_) => Value::BigInt(Rc::new(n)),
3096        }
3097    }
3098
3099    /// [`from_bigint`](Value::from_bigint) for a `BigInt` the caller holds by
3100    /// reference — an `Expr::Int` literal, which the AST owns and eval must not
3101    /// consume. The `i64` demotion is decided *before* any clone, so a literal that
3102    /// fits costs no allocation at all; only a genuinely wide one is copied. Cloning
3103    /// first and demoting after allocated a digit vector per literal evaluation and
3104    /// dropped it immediately, on a path every arithmetic loop runs.
3105    pub fn from_bigint_ref(n: &BigInt) -> Value {
3106        match i64::try_from(n) {
3107            Ok(i) => Value::I64(i),
3108            Err(_) => Value::BigInt(Rc::new(n.clone())),
3109        }
3110    }
3111
3112    /// A **host function** as a value, ready to be applied or written into a
3113    /// module's frame: the constructor a host reaches for instead of building the
3114    /// [`HostFn`] variant by hand. Unapplied, so the first argument starts the
3115    /// ordinary currying.
3116    ///
3117    /// Panics on `arity == 0` (see [`HostFn`]).
3118    pub fn host_fn(
3119        name: impl Into<Text>,
3120        arity: usize,
3121        f: impl Fn(&[Value], &Ctx) -> Result<Value, Raised> + 'static,
3122    ) -> Value {
3123        Value::HostFn {
3124            code: Rc::new(HostFn::new(name, arity, f)),
3125            args: Box::new([]),
3126        }
3127    }
3128
3129    /// A list value from its elements. The backing `rpds::Vector` is an
3130    /// implementation detail a host should not have to name: matching its version
3131    /// *and features* against the core's is a real hazard (workspace feature
3132    /// unification is global), so building one goes through here.
3133    pub fn list(elems: impl IntoIterator<Item = Value>) -> Value {
3134        Value::List(elems.into_iter().collect())
3135    }
3136
3137    /// A map value from its entries, later entries winning on an equal key. Here
3138    /// for the reason [`list`](Value::list) is.
3139    pub fn map(entries: impl IntoIterator<Item = (Value, Value)>) -> Value {
3140        let mut m = HashTrieMap::new();
3141        for (k, v) in entries {
3142            m.insert_mut(k, v);
3143        }
3144        Value::Map(m)
3145    }
3146
3147    /// The elements of a list value, or `None` for anything else — the reading
3148    /// counterpart of [`list`](Value::list), so a host can take a list apart
3149    /// without naming `rpds` either. The iterator knows its length, which is
3150    /// usually the first thing a caller checks.
3151    pub fn list_items(&self) -> Option<impl ExactSizeIterator<Item = &Value>> {
3152        match self {
3153            Value::List(elems) => Some(elems.iter()),
3154            _ => None,
3155        }
3156    }
3157
3158    /// How many more arguments a **function-shaped** value wants before it runs:
3159    /// a closure's unbound parameters, or a builtin's or host function's
3160    /// ungathered ones. `None` for everything else, *including* the values that
3161    /// are applicable without being functions — a list and a module both take a
3162    /// symbol, but that is projection, not a call with arguments outstanding.
3163    ///
3164    /// A runner uses it to notice that it under-applied: `__main = &(io, x) …`
3165    /// applied to one capability yields a partial, and the program quietly does
3166    /// nothing unless someone says so (see `docs/done/2026-08-14_elly-run.md`).
3167    pub fn remaining_arity(&self) -> Option<usize> {
3168        match self {
3169            Value::Closure { code, applied, .. } => Some(code.head.len() - *applied as usize),
3170            Value::Builtin { op, args } => Some(op.arity() - args.len()),
3171            Value::HostFn { code, args } => Some(code.arity - args.len()),
3172            _ => None,
3173        }
3174    }
3175
3176    /// The compiled module behind a [`Value::Module`], or `None` for anything
3177    /// else. A module value holds its [`Module`](EnvNode::Module) terminal rather
3178    /// than the [`ModuleData`] directly, so a host that only wants the item table
3179    /// (the Python bindings' `Obj.keys` / `in`) reaches it through here instead of
3180    /// matching on the environment's shape.
3181    pub fn module_data(&self) -> Option<&Rc<ModuleData>> {
3182        match self {
3183            Value::Module(home) => match &**home {
3184                EnvNode::Module { data, .. } => Some(data),
3185                _ => unreachable!("a module value always holds a module terminal"),
3186            },
3187            _ => None,
3188        }
3189    }
3190
3191    /// The [`ObjectData`] behind a [`Value::Object`], or `None` for anything else
3192    /// — the payload and the prototype, for a host that wants to look inside one
3193    /// (the Python bindings' `Object.data` / `.proto`).
3194    pub fn object_data(&self) -> Option<&Rc<ObjectData>> {
3195        match self {
3196            Value::Object(obj) => Some(obj),
3197            _ => None,
3198        }
3199    }
3200
3201    /// A human-readable rendering used by the eval golden tests.
3202    pub fn to_display(&self) -> String {
3203        let mut out = String::new();
3204        self.write_display(&mut out);
3205        out
3206    }
3207
3208    fn write_display(&self, out: &mut String) {
3209        match self {
3210            Value::I64(n) => out.push_str(&n.to_string()),
3211            Value::BigInt(n) => out.push_str(&n.to_string()),
3212            Value::Symbol(s) => {
3213                out.push('.');
3214                out.push_str(s);
3215            }
3216            // A string renders as a quoted, re-escaped literal (`"foo"`), so it
3217            // reads apart from the symbol `.foo` and the name `foo`.
3218            Value::Str(s) => json_str(s, out),
3219            // A list renders with brackets (`[]`, `[.a]`, `[.a, .b]`); the empty
3220            // list `[]` is unit.
3221            Value::List(vs) => {
3222                out.push('[');
3223                for (i, v) in vs.iter().enumerate() {
3224                    if i > 0 {
3225                        out.push_str(", ");
3226                    }
3227                    v.write_display(out);
3228                }
3229                out.push(']');
3230            }
3231            // A map is unordered, so entries render sorted by rendered key text
3232            // (ties broken by rendered value) for a stable, human-meaningful dump:
3233            // `{}` empty, else `{ .a: 1, .b: 2 }`.
3234            Value::Map(m) => {
3235                if m.is_empty() {
3236                    out.push_str("{}");
3237                } else {
3238                    let mut entries: Vec<(String, String)> = m
3239                        .iter()
3240                        .map(|(k, v)| {
3241                            let mut ks = String::new();
3242                            k.write_display(&mut ks);
3243                            let mut vs = String::new();
3244                            v.write_display(&mut vs);
3245                            (ks, vs)
3246                        })
3247                        .collect();
3248                    entries.sort();
3249                    out.push('{');
3250                    for (i, (ks, vs)) in entries.iter().enumerate() {
3251                        out.push_str(if i > 0 { ", " } else { " " });
3252                        out.push_str(ks);
3253                        out.push_str(": ");
3254                        out.push_str(vs);
3255                    }
3256                    out.push_str(" }");
3257                }
3258            }
3259            Value::Closure { .. } => out.push_str("<closure>"),
3260            Value::Builtin { op, .. } => {
3261                out.push_str("<builtin ");
3262                out.push_str(op.name());
3263                out.push('>');
3264            }
3265            // Named, like a builtin, and by the same reasoning: the name is what
3266            // a reader can act on. It is the host's, not the language's, so it
3267            // carries no `__`.
3268            Value::HostFn { code, .. } => {
3269                out.push_str("<hostfn ");
3270                out.push_str(code.name.as_str());
3271                out.push('>');
3272            }
3273            Value::Module(_) => out.push_str("<module>"),
3274            // Opaque, like a closure, and named only by its own declaration:
3275            // `<const .true>`. The module it came from is *not* named — a
3276            // module's canonical name is host-shaped (a resolver path), so
3277            // printing it would make output depend on how a host names files,
3278            // and a module value itself does not show one either.
3279            Value::Iota { home, index } => {
3280                out.push_str("<const .");
3281                if let EnvNode::Module { data, .. } = &**home {
3282                    if let Some(name) = data.iota(*index) {
3283                        out.push_str(name);
3284                    }
3285                }
3286                out.push('>');
3287            }
3288            // The payload beside an unnamed kind: `<object 0>`. Naming the
3289            // prototype (`<Bool 0>`) waits on modules having a short display name
3290            // — a canonical name is host-shaped (a resolver path), so printing it
3291            // would make output depend on how a host names files, which is why an
3292            // iota renders `<const .true>` and a module `<module>`.
3293            //
3294            // The payload *is* shown: a debugger and an embedding host sit outside
3295            // the language's module scope. No builtin renders a value to a string,
3296            // so Elly code cannot reach a payload this way.
3297            Value::Object(obj) => {
3298                out.push_str("<object ");
3299                obj.data.write_display(out);
3300                out.push('>');
3301            }
3302        }
3303    }
3304}