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