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