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