Skip to main content

elly_core/
builtins.rs

1//! Native builtins of the reserved `__` namespace: the [`Builtin`] enum,
2//! the [`MODULES`] table grouping them into modules, and [`invoke`], which runs a
3//! fully-applied builtin. Split from `eval.rs`; the evaluator drives these via
4//! `apply1`/`apply_n`.
5
6use alloc::borrow::Cow;
7use alloc::rc::Rc;
8use alloc::string::{String, ToString};
9use core::cmp::Ordering;
10
11use num_bigint::{BigInt, Sign};
12use rpds::{HashTrieMap, Vector};
13
14use crate::ast::{Text, TyKind};
15use crate::eval::{self, proto_of, Ctx, EnvNode, EnvRef, Raised};
16use crate::value::{ObjectData, Value};
17
18/// A native builtin operation in the reserved `__` namespace. All are curried.
19#[allow(clippy::enum_variant_names)]
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum Builtin {
22    IntAdd,
23    IntSub,
24    IntMul,
25    IntPow,
26    IntDivrem,
27    IntFor,
28    /// Relational tests on two integers, each returning a [`Bool`](Builtin::Bool)
29    /// iota. These feed `if` through receiver dispatch (`n.lt 60`).
30    IntEq,
31    IntLt,
32    IntLe,
33    IntGt,
34    IntGe,
35    ErrRaise,
36    ErrCatch,
37    Match,
38    ListSize,
39    ListGet,
40    ListAppend,
41    ListWith,
42    ListSplit,
43    Eq,
44    MapNil,
45    MapWith,
46    MapGet,
47    MapGets,
48    MapFor,
49    MapMerge,
50    MapCat,
51    MapWithout,
52    MapSize,
53    IntStr,
54    IntParse,
55    StrPack,
56    StrUnpack,
57    SymFrom,
58    SymStr,
59    ModName,
60    /// `__Mod.exports <Mod>`: returns the module's exported item names as a
61    /// sorted list of symbols. A local, private iota, or non-re-exported import
62    /// is not an export. Reads the frozen table without evaluating item bodies.
63    /// Non-modules raise `.not_a_module`.
64    ModExports,
65    /// `__proto v`: the prototype of any value: an object's own, or the
66    /// builtin module for other kinds. A bare builtin that cuts across kinds.
67    /// Total, it serves as the classifier `as <Proto>` and the basis for
68    /// object design. It grants no authority to construct or unwrap.
69    Proto,
70    /// `__repr v`: the display string of any value, matching
71    /// [`to_display`](Value::to_display). A bare builtin that cuts across kinds.
72    Repr,
73    /// Object constructor `(prototype, payload)`. Not reachable by name;
74    /// only accessible via the [`__new`](crate::HomeLeaf::New) leaf, which
75    /// supplies the prototype from the current module.
76    ObjNew,
77    /// Object payload reader `(prototype, object)`. Accessible only via the
78    /// [`__value`](crate::HomeLeaf::Value) leaf. Refuses objects whose prototype
79    /// does not match the supplied one (`.foreign_object`), preventing methods
80    /// from unwrapping foreign objects.
81    ObjValue,
82    // The **builtin modules** — a 0-arity builtin each, naming the module that
83    // gathers the operations above as its items (see [`MODULES`]).
84    Int,
85    Str,
86    Sym,
87    List,
88    Map,
89    Err,
90    Mod,
91    /// `__Bool`, the two-iota boolean module. Its `const` declarations are
92    /// [`true`](Builtin::BoolTrue) and [`false`](Builtin::BoolFalse), name-sorted
93    /// so `false` is iota index 0 and `true` is index 1 (see `bool_value`); it
94    /// re-exports each as an item forwarding to it — so `Bool.true` resolves like a
95    /// user module's re-exported iota, while `__true` / `__false` name them
96    /// directly (see [`from_name`](Builtin::from_name)). Its logic operations
97    /// [`and`](Builtin::BoolAnd) / [`or`](Builtin::BoolOr) / [`not`](Builtin::BoolNot)
98    /// compose them; the relational `__Int.*` tests produce them.
99    Bool,
100    /// `__Bool.true` / `__true`: the true iota, an arity-0 value builtin that fires
101    /// on reference to `bool_value(ctx, true)`. Both spellings fold to this one op,
102    /// so they are the *same* iota of the same cached `__Bool` instance.
103    BoolTrue,
104    /// `__Bool.false` / `__false`: the false iota — [`BoolTrue`](Builtin::BoolTrue)'s
105    /// counterpart.
106    BoolFalse,
107    /// `__Bool.and` / `or` / `not` — strict logic over two [`Bool`](Builtin::Bool)
108    /// iotas (one, for `not`); a non-`Bool` argument raises `.not_a_bool`.
109    BoolAnd,
110    BoolOr,
111    BoolNot,
112    /// `__Fun`, the prototype of every callable kind — a closure, a builtin, and
113    /// a host function. It has no items yet: `docs/todo/elly-function-arity.md`
114    /// is what gives it `nargs` / `nfree` / `nrets`. It exists for the totality of
115    /// [`Proto`](Builtin::Proto), which is enough on its own to make it a module.
116    Fun,
117    /// `__Unit`, the prototype of the [unit](Value::Unit) value `()`. It has no
118    /// items — a nominal type only, exactly like [`Fun`](Builtin::Fun) — and
119    /// exists so every value, unit included, answers [`Proto`](Builtin::Proto).
120    Unit,
121}
122
123/// A builtin module's item table: `(item name, the builtin it names)`, **sorted
124/// by name** — the order a [`ModuleData`] wants, and what
125/// [`member`](Builtin::member) binary-searches.
126pub(crate) type Members = &'static [(&'static str, Builtin)];
127
128/// A builtin module's `const` declarations — its **iotas** — as name-sorted static
129/// strings. Only [`__Bool`](Builtin::Bool) has any today; every other module's is
130/// empty. The names populate [`ModuleData::iotas`], which is what an iota's
131/// `<const .name>` rendering reads (see [`Value::write_display`]).
132pub(crate) type Iotas = &'static [&'static str];
133
134/// The **builtin modules**, in cache-slot order (see [`Ctx::builtin_module`]):
135/// the 0-arity builtin naming each module, its shadowable bare alias, and its
136/// items. The `__<Ns>_<op>` naming convention the flat builtins used to spell out
137/// lives here instead — `__Int.add` is the module `__Int`'s item `add` — so this
138/// table is the whole of the `__` namespace's structure.
139///
140/// `__match`, `__eq` and `__proto` are not in it: they cut across kinds and
141/// belong to no module.
142const MODULES: &[(Builtin, &str, Members, Iotas)] = &[
143    (
144        Builtin::Int,
145        "Int",
146        &[
147            ("add", Builtin::IntAdd),
148            ("divrem", Builtin::IntDivrem),
149            ("eq", Builtin::IntEq),
150            ("for", Builtin::IntFor),
151            ("ge", Builtin::IntGe),
152            ("gt", Builtin::IntGt),
153            ("le", Builtin::IntLe),
154            ("lt", Builtin::IntLt),
155            ("mul", Builtin::IntMul),
156            ("parse", Builtin::IntParse),
157            ("pow", Builtin::IntPow),
158            ("str", Builtin::IntStr),
159            ("sub", Builtin::IntSub),
160        ],
161        &[],
162    ),
163    (
164        Builtin::Str,
165        "Str",
166        &[("pack", Builtin::StrPack), ("unpack", Builtin::StrUnpack)],
167        &[],
168    ),
169    (
170        Builtin::Sym,
171        "Sym",
172        &[("from", Builtin::SymFrom), ("str", Builtin::SymStr)],
173        &[],
174    ),
175    (
176        Builtin::List,
177        "List",
178        &[
179            ("append", Builtin::ListAppend),
180            ("get", Builtin::ListGet),
181            ("size", Builtin::ListSize),
182            ("split", Builtin::ListSplit),
183            ("with", Builtin::ListWith),
184        ],
185        &[],
186    ),
187    (
188        Builtin::Map,
189        "Map",
190        &[
191            ("cat", Builtin::MapCat),
192            ("for", Builtin::MapFor),
193            ("get", Builtin::MapGet),
194            ("gets", Builtin::MapGets),
195            ("merge", Builtin::MapMerge),
196            ("nil", Builtin::MapNil),
197            ("size", Builtin::MapSize),
198            ("with", Builtin::MapWith),
199            ("without", Builtin::MapWithout),
200        ],
201        &[],
202    ),
203    (
204        Builtin::Err,
205        "Err",
206        &[("catch", Builtin::ErrCatch), ("raise", Builtin::ErrRaise)],
207        &[],
208    ),
209    (
210        Builtin::Mod,
211        "Mod",
212        &[("exports", Builtin::ModExports), ("name", Builtin::ModName)],
213        &[],
214    ),
215    // The two-iota boolean module. `true` / `false` are both iotas (`const`
216    // declarations, populating the iota table for rendering) and items that
217    // re-export them — the same double declaration a user's `false = const` makes
218    // — so `Bool.true` resolves through the ordinary member path while `__true`
219    // names the iota directly. The iotas are name-sorted: `false` is index 0,
220    // `true` index 1 (see [`bool_value`]).
221    (
222        Builtin::Bool,
223        "Bool",
224        &[
225            ("and", Builtin::BoolAnd),
226            ("false", Builtin::BoolFalse),
227            ("not", Builtin::BoolNot),
228            ("or", Builtin::BoolOr),
229            ("true", Builtin::BoolTrue),
230        ],
231        &["false", "true"],
232    ),
233    // No items yet, and a module all the same: it is the prototype the three
234    // callable kinds answer `__proto` with (see [`Builtin::Fun`]).
235    (Builtin::Fun, "Fun", &[], &[]),
236    // Likewise item-less: the nominal type of the unit value `()`.
237    (Builtin::Unit, "Unit", &[], &[]),
238];
239
240/// How many builtin modules there are — the width of the [`Ctx`] cache.
241pub(crate) const NMODULES: usize = MODULES.len();
242
243impl Builtin {
244    /// Every nameable builtin in declaration order; the source of truth for
245    /// tooling enumerating the `__` namespace. Keep in sync with the enum.
246    ///
247    /// [`ObjNew`](Builtin::ObjNew) and [`ObjValue`](Builtin::ObjValue) are
248    /// omitted as no name resolves to them; they only render via [`name`](Builtin::name).
249    pub const ALL: &'static [Builtin] = &[
250        Builtin::IntAdd,
251        Builtin::IntSub,
252        Builtin::IntMul,
253        Builtin::IntPow,
254        Builtin::IntDivrem,
255        Builtin::IntFor,
256        Builtin::IntEq,
257        Builtin::IntLt,
258        Builtin::IntLe,
259        Builtin::IntGt,
260        Builtin::IntGe,
261        Builtin::ErrRaise,
262        Builtin::ErrCatch,
263        Builtin::Match,
264        Builtin::ListSize,
265        Builtin::ListGet,
266        Builtin::ListAppend,
267        Builtin::ListWith,
268        Builtin::ListSplit,
269        Builtin::Eq,
270        Builtin::MapNil,
271        Builtin::MapWith,
272        Builtin::MapGet,
273        Builtin::MapGets,
274        Builtin::MapFor,
275        Builtin::MapMerge,
276        Builtin::MapCat,
277        Builtin::MapWithout,
278        Builtin::MapSize,
279        Builtin::IntStr,
280        Builtin::IntParse,
281        Builtin::StrPack,
282        Builtin::StrUnpack,
283        Builtin::SymFrom,
284        Builtin::SymStr,
285        Builtin::ModName,
286        Builtin::ModExports,
287        Builtin::Proto,
288        Builtin::Repr,
289        Builtin::Int,
290        Builtin::Str,
291        Builtin::Sym,
292        Builtin::List,
293        Builtin::Map,
294        Builtin::Err,
295        Builtin::Mod,
296        Builtin::Bool,
297        Builtin::BoolTrue,
298        Builtin::BoolFalse,
299        Builtin::BoolAnd,
300        Builtin::BoolOr,
301        Builtin::BoolNot,
302        Builtin::Fun,
303        Builtin::Unit,
304    ];
305
306    /// Resolves a `__`-name to its builtin: a bare name (`__eq`), a builtin
307    /// module (`__Int`), or a module member in dotted form (`__Int.add`).
308    ///
309    /// Since Muon symbols cannot contain dots, the parser reaches members via
310    /// [`member`](Self::member) after folding. This method is used by tooling and
311    /// the `Builtin::ALL` round-trip.
312    pub fn from_name(name: &str) -> Option<Builtin> {
313        if let Some((ns, item)) = name.split_once('.') {
314            return Builtin::from_name(ns)?.member(item);
315        }
316        Some(match name {
317            "__match" => Builtin::Match,
318            "__eq" => Builtin::Eq,
319            "__proto" => Builtin::Proto,
320            "__repr" => Builtin::Repr,
321            // The two boolean iotas name themselves in the closed `__` namespace,
322            // beside `Bool.true` / `Bool.false`: one Muon `<sym>` each, so a
323            // comparand (`&(= __true)`) can name one where the dotted member is
324            // two items. Both spellings fold to the same op, hence the same iota.
325            "__true" => Builtin::BoolTrue,
326            "__false" => Builtin::BoolFalse,
327            "__Int" => Builtin::Int,
328            "__Str" => Builtin::Str,
329            "__Sym" => Builtin::Sym,
330            "__List" => Builtin::List,
331            "__Map" => Builtin::Map,
332            "__Err" => Builtin::Err,
333            "__Mod" => Builtin::Mod,
334            "__Bool" => Builtin::Bool,
335            "__Fun" => Builtin::Fun,
336            "__Unit" => Builtin::Unit,
337            _ => return None,
338        })
339    }
340
341    /// The builtin module this bare alias names (e.g. `Int` → `__Int`).
342    /// Aliases are the resolver's bottom tier (the prelude), consulted last to
343    /// ensure they are shadowed by lexical bindings.
344    pub fn from_alias(name: &str) -> Option<Builtin> {
345        MODULES
346            .iter()
347            .find(|(_, alias, _, _)| *alias == name)
348            .map(|(op, _, _, _)| *op)
349    }
350
351    /// The items of this builtin module, or `None` if not a module.
352    pub(crate) fn items(self) -> Option<Members> {
353        MODULES
354            .iter()
355            .find(|(op, _, _, _)| *op == self)
356            .map(|(_, _, items, _)| *items)
357    }
358
359    /// The iota names of this builtin module, or `None` if not a module.
360    /// Empty for all but [`__Bool`](Builtin::Bool).
361    pub(crate) fn iotas(self) -> Option<Iotas> {
362        MODULES
363            .iter()
364            .find(|(op, _, _, _)| *op == self)
365            .map(|(_, _, _, iotas)| *iotas)
366    }
367
368    /// The cache slot for this builtin module in a [`Ctx`], or `None`.
369    /// Positional and stable for the life of the process.
370    pub(crate) fn module_slot(self) -> Option<usize> {
371        MODULES.iter().position(|(op, _, _, _)| *op == self)
372    }
373
374    /// The item of this builtin module matching `name`, if any. Returns `None`
375    /// if the name is not held or the builtin is not a module. Safe to call on
376    /// any callee of a call spine.
377    pub fn member(self, name: &str) -> Option<Builtin> {
378        let items = self.items()?;
379        items
380            .binary_search_by(|(item, _)| (*item).cmp(name))
381            .ok()
382            .map(|i| items[i].1)
383    }
384
385    /// The value kind this builtin module is the prototype of, enabling `as <Proto>`
386    /// patterns to compile to discriminant tests. Returns `None` if not a module
387    /// or for `__Err` (no value has `__Err` as its prototype).
388    pub fn proto_kind(self) -> Option<TyKind> {
389        Some(match self {
390            Builtin::Int => TyKind::Int,
391            Builtin::Str => TyKind::Str,
392            Builtin::Sym => TyKind::Sym,
393            Builtin::List => TyKind::List,
394            Builtin::Map => TyKind::Map,
395            Builtin::Mod => TyKind::Mod,
396            Builtin::Fun => TyKind::Fun,
397            Builtin::Unit => TyKind::Unit,
398            Builtin::Bool => TyKind::Bool,
399            _ => return None,
400        })
401    }
402
403    /// Number of arguments the op consumes before firing. Arity 0 ops are bare
404    /// values that fire on reference (e.g. `__Map.nil` or builtin modules).
405    pub(crate) fn arity(self) -> usize {
406        match self {
407            Builtin::MapNil
408            | Builtin::Int
409            | Builtin::Str
410            | Builtin::Sym
411            | Builtin::List
412            | Builtin::Map
413            | Builtin::Err
414            | Builtin::Mod
415            | Builtin::Bool
416            // The two boolean iotas are values: they fire on reference.
417            | Builtin::BoolTrue
418            | Builtin::BoolFalse
419            | Builtin::Fun
420            | Builtin::Unit => 0,
421            Builtin::Proto
422            | Builtin::Repr
423            | Builtin::ErrRaise
424            | Builtin::ListSize
425            | Builtin::MapSize
426            | Builtin::IntStr
427            | Builtin::IntParse
428            | Builtin::StrPack
429            | Builtin::StrUnpack
430            | Builtin::SymFrom
431            | Builtin::SymStr
432            | Builtin::BoolNot
433            | Builtin::ModName
434            | Builtin::ModExports => 1,
435            Builtin::IntAdd
436            | Builtin::IntSub
437            | Builtin::IntMul
438            | Builtin::IntPow
439            | Builtin::IntDivrem
440            | Builtin::IntEq
441            | Builtin::IntLt
442            | Builtin::IntLe
443            | Builtin::IntGt
444            | Builtin::IntGe
445            | Builtin::BoolAnd
446            | Builtin::BoolOr => 2,
447            // `__match clauses val`: strict in the clause list and the subject.
448            // `__new proto data` / `__value proto obj` — the prototype is the
449            // first argument, and the home-module leaf has already supplied it.
450            Builtin::ObjNew
451            | Builtin::ObjValue
452            | Builtin::Match
453            | Builtin::ErrCatch
454            | Builtin::ListGet
455            | Builtin::ListAppend
456            | Builtin::ListSplit
457            | Builtin::MapGet
458            | Builtin::MapGets
459            | Builtin::MapCat
460            | Builtin::MapWithout => 2,
461            Builtin::ListWith | Builtin::MapWith | Builtin::MapFor | Builtin::MapMerge => 3,
462            Builtin::IntFor | Builtin::Eq => 4,
463        }
464    }
465
466    /// Whether argument `i` is a callback that is only ever applied, never stored.
467    /// Literals in such positions use [`Captures::Chain`] to share the defining
468    /// environment instead of copying a frame.
469    ///
470    /// Must stay in sync with [`invoke`]: arguments listed here must only be
471    /// reached via `apply1`/`apply_n`.
472    pub(crate) fn applies_arg(self, i: usize) -> bool {
473        match self {
474            // `__Err.catch handler thunk` — both are run by the catch.
475            Builtin::ErrCatch => i == 0 || i == 1,
476            // `__eq a b then else` — one of the two branches is forced.
477            Builtin::Eq => i == 2 || i == 3,
478            // `__Int.for from to init f` — the fold callback.
479            Builtin::IntFor => i == 3,
480            // `__Map.for m init f` / `__Map.merge m1 m2 f` — the fold callbacks.
481            Builtin::MapFor | Builtin::MapMerge => i == 2,
482            _ => false,
483        }
484    }
485
486    /// Whether argument `i` is a list of callbacks applied in order (e.g.
487    /// `__match` clauses). Literal abstractions in such lists use [`Captures::Chain`].
488    pub(crate) fn applies_elements_of(self, i: usize) -> bool {
489        matches!(self, Builtin::Match) && i == 0
490    }
491
492    /// The reserved `__`-name for this builtin. Module members are dotted
493    /// (e.g. `__Int.add`).
494    pub fn name(self) -> &'static str {
495        match self {
496            Builtin::IntAdd => "__Int.add",
497            Builtin::IntSub => "__Int.sub",
498            Builtin::IntMul => "__Int.mul",
499            Builtin::IntPow => "__Int.pow",
500            Builtin::IntDivrem => "__Int.divrem",
501            Builtin::IntFor => "__Int.for",
502            Builtin::IntEq => "__Int.eq",
503            Builtin::IntLt => "__Int.lt",
504            Builtin::IntLe => "__Int.le",
505            Builtin::IntGt => "__Int.gt",
506            Builtin::IntGe => "__Int.ge",
507            Builtin::ErrRaise => "__Err.raise",
508            Builtin::ErrCatch => "__Err.catch",
509            Builtin::Match => "__match",
510            Builtin::ListSize => "__List.size",
511            Builtin::ListGet => "__List.get",
512            Builtin::ListAppend => "__List.append",
513            Builtin::ListWith => "__List.with",
514            Builtin::ListSplit => "__List.split",
515            Builtin::Eq => "__eq",
516            Builtin::MapNil => "__Map.nil",
517            Builtin::MapWith => "__Map.with",
518            Builtin::MapGet => "__Map.get",
519            Builtin::MapGets => "__Map.gets",
520            Builtin::MapFor => "__Map.for",
521            Builtin::MapMerge => "__Map.merge",
522            Builtin::MapCat => "__Map.cat",
523            Builtin::MapWithout => "__Map.without",
524            Builtin::MapSize => "__Map.size",
525            Builtin::IntStr => "__Int.str",
526            Builtin::IntParse => "__Int.parse",
527            Builtin::StrPack => "__Str.pack",
528            Builtin::StrUnpack => "__Str.unpack",
529            Builtin::SymFrom => "__Sym.from",
530            Builtin::SymStr => "__Sym.str",
531            Builtin::ModName => "__Mod.name",
532            Builtin::ModExports => "__Mod.exports",
533            Builtin::Proto => "__proto",
534            Builtin::Repr => "__repr",
535            // The two capabilities render under the leaf that produced them —
536            // there is no other way to hold one, so this is the name a reader
537            // wrote (see [`ALL`](Builtin::ALL) for why they are not in it).
538            Builtin::ObjNew => "__new",
539            Builtin::ObjValue => "__value",
540            Builtin::Int => "__Int",
541            Builtin::Str => "__Str",
542            Builtin::Sym => "__Sym",
543            Builtin::List => "__List",
544            Builtin::Map => "__Map",
545            Builtin::Err => "__Err",
546            Builtin::Mod => "__Mod",
547            Builtin::Bool => "__Bool",
548            // The members render dotted, as every module member does; `__true` /
549            // `__false` are the extra bare spellings [`from_name`](Self::from_name)
550            // also accepts, the way `Int` aliases `__Int`.
551            Builtin::BoolTrue => "__Bool.true",
552            Builtin::BoolFalse => "__Bool.false",
553            Builtin::BoolAnd => "__Bool.and",
554            Builtin::BoolOr => "__Bool.or",
555            Builtin::BoolNot => "__Bool.not",
556            Builtin::Fun => "__Fun",
557            Builtin::Unit => "__Unit",
558        }
559    }
560}
561
562/// Runs a fully-applied builtin. Borrows arguments to avoid `Value::Builtin`
563/// allocation when a call spine saturates a builtin exactly.
564pub(crate) fn invoke(op: Builtin, args: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
565    match op {
566        Builtin::IntAdd => int_arith(&args[0], &args[1], i64::checked_add, |a, b| a + b),
567        Builtin::IntSub => int_arith(&args[0], &args[1], i64::checked_sub, |a, b| a - b),
568        Builtin::IntMul => int_arith(&args[0], &args[1], i64::checked_mul, |a, b| a * b),
569        // `x ^ y`, `y >= 0`. A negative exponent has no integer value.
570        Builtin::IntPow => {
571            let e = u32::try_from(&*as_big(&args[1])?)
572                .map_err(|_| Raised::symbol("negative_exponent"))?;
573            // Small-base fast path: `i64::checked_pow`, promoting on overflow.
574            if let Value::I64(x) = &args[0] {
575                if let Some(z) = x.checked_pow(e) {
576                    return Ok(Value::I64(z));
577                }
578            }
579            Ok(Value::from_bigint(as_big(&args[0])?.pow(e)))
580        }
581        Builtin::IntDivrem => {
582            // Fast path: both operands fit `i64`. `i64::MIN / -1` overflows, so on a
583            // `None` from either `checked_*` we fall through to the `BigInt` path.
584            if let (Value::I64(x), Value::I64(y)) = (&args[0], &args[1]) {
585                if *y == 0 {
586                    return Err(Raised::symbol("div_by_zero"));
587                }
588                if let (Some(q), Some(r)) = (x.checked_div_euclid(*y), x.checked_rem_euclid(*y)) {
589                    let mut t = Vector::new();
590                    t.push_back_mut(Value::I64(q));
591                    t.push_back_mut(Value::I64(r));
592                    return Ok(Value::List(t));
593                }
594            }
595            let (q, r) = div_rem_euclid(&*as_big(&args[0])?, &*as_big(&args[1])?)?;
596            let mut t = Vector::new();
597            t.push_back_mut(Value::from_bigint(q));
598            t.push_back_mut(Value::from_bigint(r));
599            Ok(Value::List(t))
600        }
601        // The relational tests: strict in two integers, each yielding a `Bool`
602        // iota. An `i64` fast path (a native compare) falls through to `BigInt`
603        // only for a wide operand, and a non-integer raises `.not_an_int` through
604        // `as_big` — the same strictness the arithmetic ops have.
605        Builtin::IntEq => int_cmp(&args[0], &args[1], ctx, |o| o == Ordering::Equal),
606        Builtin::IntLt => int_cmp(&args[0], &args[1], ctx, |o| o == Ordering::Less),
607        Builtin::IntLe => int_cmp(&args[0], &args[1], ctx, |o| o != Ordering::Greater),
608        Builtin::IntGt => int_cmp(&args[0], &args[1], ctx, |o| o == Ordering::Greater),
609        Builtin::IntGe => int_cmp(&args[0], &args[1], ctx, |o| o != Ordering::Less),
610        // The boolean iotas fire on reference, deriving from the cached `__Bool`.
611        Builtin::BoolTrue => Ok(bool_value(ctx, true)),
612        Builtin::BoolFalse => Ok(bool_value(ctx, false)),
613        // Strict logic: both operands are forced (they arrive already evaluated),
614        // so there is no short-circuit — nor a need for one, the operands being
615        // values. A non-`Bool` raises `.not_a_bool`.
616        Builtin::BoolAnd => Ok(bool_value(
617            ctx,
618            as_bool(&args[0], ctx)? && as_bool(&args[1], ctx)?,
619        )),
620        Builtin::BoolOr => Ok(bool_value(
621            ctx,
622            as_bool(&args[0], ctx)? || as_bool(&args[1], ctx)?,
623        )),
624        Builtin::BoolNot => Ok(bool_value(ctx, !as_bool(&args[0], ctx)?)),
625        // ascending fold over `[from, to)`, index first: `onEach i acc`.
626        // `from >= to` runs zero iterations and returns the initial state.
627        Builtin::IntFor => {
628            let f = &args[3];
629            let mut acc = args[2].clone();
630            // Fast path: both bounds fit `i64` — an inline counter, no per-iteration
631            // `BigInt` allocation. (`i` only ever reaches `to <= i64::MAX`, so the
632            // `i += 1` after the last iteration cannot overflow.)
633            if let (Value::I64(from), Value::I64(to)) = (&args[0], &args[1]) {
634                let (from, to) = (*from, *to);
635                let mut i = from;
636                while i < to {
637                    let with_i = eval::apply1(f.clone(), Value::I64(i), ctx)?;
638                    acc = eval::apply1(with_i, acc, ctx)?;
639                    i += 1;
640                }
641                return Ok(acc);
642            }
643            // General path: a bound is a wide `BigInt` (an astronomically long loop).
644            let to = as_big(&args[1])?.into_owned();
645            let mut i = as_big(&args[0])?.into_owned();
646            while i < to {
647                let with_i = eval::apply1(f.clone(), Value::from_bigint(i.clone()), ctx)?;
648                acc = eval::apply1(with_i, acc, ctx)?;
649                i += 1;
650            }
651            Ok(acc)
652        }
653        // `onEqual () if a == b else onDiff ()` — structural for data, by identity
654        // for callables (see `impl PartialEq for Value`). Nullary-thunk branches, so only
655        // the taken side runs; the operands may be any values and never raise.
656        Builtin::Eq => {
657            let taken = if args[0] == args[1] {
658                &args[2]
659            } else {
660                &args[3]
661            };
662            eval::apply1(taken.clone(), args[0].clone(), ctx)
663        }
664        // Raise the (already-evaluated) argument as a genuine error, unwinding to
665        // the nearest catch.
666        Builtin::ErrRaise => Err(Raised::Error(args[0].clone())),
667        // Force the wrapped thunk; on *any* raise, hand the payload to the handler
668        // — `__Err.catch` is the universal boundary, so it catches a refutation
669        // too, delivering the original error it decayed to. A raise from the
670        // handler itself propagates (not re-caught here).
671        Builtin::ErrCatch => match eval::apply1(args[1].clone(), unit(), ctx) {
672            Ok(v) => Ok(v),
673            Err(r) => eval::apply1(args[0].clone(), r.into_value(), ctx),
674        },
675        // The refutable form: `__match clauses val`. Try each clause (a matcher,
676        // typically `&(<pat>) <body>`) against `val` in order; the first whose
677        // pattern *matches* returns its body's value, and no later clause runs. A
678        // clause that *refutes* (a `NoMatch`) is skipped and the next is tried;
679        // every other raise — a real `Error` from the body or a `= <expr>`
680        // sub-expression — propagates. Falling off the end (an empty list, or all
681        // clauses refuting) itself refutes `.no_match`, so an uncaught `__match`
682        // surfaces `.no_match` while a nested one can still be caught by an outer
683        // matcher. `__match` is the *only* boundary
684        // that singles out `NoMatch` (which user code cannot forge), making "only
685        // pattern mismatches fall through" exact. See `docs/elly-spec.md` § patterns.
686        Builtin::Match => {
687            for clause in list(&args[0])?.iter() {
688                match eval::apply1(clause.clone(), args[1].clone(), ctx) {
689                    Ok(v) => return Ok(v),
690                    Err(Raised::NoMatch(_)) => continue,
691                    Err(e) => return Err(e),
692                }
693            }
694            Err(Raised::NoMatch(Value::Symbol(Text::from_static(
695                "no_match",
696            ))))
697        }
698        // Element count of a list.
699        Builtin::ListSize => Ok(Value::I64(list(&args[0])?.len() as i64)),
700        // Element at a 0-based integer index; out of range (or negative) raises
701        // `.projection_out_of_range`, generalizing `.N` projection to a computed
702        // index.
703        Builtin::ListGet => {
704            let t = list(&args[0])?;
705            index_of(&args[1])?
706                .and_then(|idx| t.get(idx))
707                .cloned()
708                .ok_or_else(|| Raised::symbol("projection_out_of_range"))
709        }
710        // Concatenate two lists: `[a…] ++ [b…]`.
711        Builtin::ListAppend => {
712            let mut r = list(&args[0])?.clone();
713            for e in list(&args[1])?.iter() {
714                r.push_back_mut(e.clone());
715            }
716            Ok(Value::List(r))
717        }
718        // Replace the element at index `i` with `v`, returning a new list;
719        // out of range (or a negative index) raises `.projection_out_of_range`.
720        Builtin::ListWith => {
721            let t = list(&args[0])?;
722            let v = args[2].clone();
723            index_of(&args[1])?
724                .and_then(|idx| t.set(idx, v))
725                .map(Value::List)
726                .ok_or_else(|| Raised::symbol("projection_out_of_range"))
727        }
728        // Split a list at position `n` into `[left, right]` (left holds the
729        // first `n` elements). `n` must be in `[0, size]`, else
730        // `.projection_out_of_range`.
731        Builtin::ListSplit => {
732            let t = list(&args[1])?;
733            let n = index_of(&args[0])?
734                .filter(|&n| n <= t.len())
735                .ok_or_else(|| Raised::symbol("projection_out_of_range"))?;
736            let mut left = Vector::new();
737            let mut right = Vector::new();
738            for (idx, e) in t.iter().enumerate() {
739                if idx < n {
740                    left.push_back_mut(e.clone());
741                } else {
742                    right.push_back_mut(e.clone());
743                }
744            }
745            let mut pair = Vector::new();
746            pair.push_back_mut(Value::List(left));
747            pair.push_back_mut(Value::List(right));
748            Ok(Value::List(pair))
749        }
750        // The empty map (a 0-arity value; `args` is empty).
751        Builtin::MapNil => Ok(Value::Map(HashTrieMap::new())),
752        // `m` with `k` bound to `v` (overwriting any prior binding). Any value is
753        // a key — no key restriction, no bad-key error.
754        Builtin::MapWith => {
755            let m = map(&args[0])?;
756            Ok(Value::Map(m.insert(args[1].clone(), args[2].clone())))
757        }
758        // Value at `k`; a missing key raises `.missing_key` (a catchable tag).
759        Builtin::MapGet => {
760            let m = map(&args[0])?;
761            m.get(&args[1])
762                .cloned()
763                .ok_or_else(|| Raised::symbol("missing_key"))
764        }
765        // Parallel projection: a list of the values for a list of keys; any
766        // missing key raises `.missing_key`.
767        Builtin::MapGets => {
768            let m = map(&args[0])?;
769            let mut out = Vector::new();
770            for k in list(&args[1])?.iter() {
771                let v = m
772                    .get(k)
773                    .cloned()
774                    .ok_or_else(|| Raised::symbol("missing_key"))?;
775                out.push_back_mut(v);
776            }
777            Ok(Value::List(out))
778        }
779        // Fold over the map (unspecified order): `onEach key value state -> state`.
780        Builtin::MapFor => {
781            let m = map(&args[0])?;
782            let f = &args[2];
783            let mut acc = args[1].clone();
784            for (k, v) in m.iter() {
785                let with_k = eval::apply1(f.clone(), k.clone(), ctx)?;
786                let with_v = eval::apply1(with_k, v.clone(), ctx)?;
787                acc = eval::apply1(with_v, acc, ctx)?;
788            }
789            Ok(acc)
790        }
791        // Generic merge over the union of keys. For each key the callback gets the
792        // key and each side's value as an option (`[]` none / `[v]` some) and
793        // returns an option — `[]` drops the key, `[v]` sets it.
794        Builtin::MapMerge => {
795            let m1 = map(&args[0])?;
796            let m2 = map(&args[1])?;
797            let f = &args[2];
798            let mut result = HashTrieMap::new();
799            // Every key in the union, visited once (order unspecified): all of
800            // `m1`'s keys, then `m2`'s keys that `m1` lacks.
801            let union = m1.keys().chain(m2.keys().filter(|k| !m1.contains_key(*k)));
802            for k in union {
803                let k = k.clone();
804                let with_k = eval::apply1(f.clone(), k.clone(), ctx)?;
805                let with_l = eval::apply1(with_k, option(m1.get(&k)), ctx)?;
806                let out = eval::apply1(with_l, option(m2.get(&k)), ctx)?;
807                if let Some(v) = un_option(&out)? {
808                    result.insert_mut(k, v);
809                }
810            }
811            Ok(Value::Map(result))
812        }
813        // Union with `b` winning on a key conflict (a `__Map.merge` special case).
814        Builtin::MapCat => {
815            let mut r = map(&args[0])?.clone();
816            for (k, v) in map(&args[1])?.iter() {
817                r.insert_mut(k.clone(), v.clone());
818            }
819            Ok(Value::Map(r))
820        }
821        // `m` with `k` removed (a no-op if absent); the persistent-update
822        // counterpart of `__Map.with`.
823        Builtin::MapWithout => {
824            let m = map(&args[0])?;
825            Ok(Value::Map(m.remove(&args[1])))
826        }
827        // Element count of a map, symmetric with `__List.size`.
828        Builtin::MapSize => Ok(Value::I64(map(&args[0])?.size() as i64)),
829        // Canonical signed decimal of an integer (`15` → `"15"`); the Int half of
830        // the scalar→text bridge. A non-int raises `.not_an_int`.
831        Builtin::IntStr => {
832            let s = match &args[0] {
833                Value::I64(x) => x.to_string(),
834                Value::BigInt(n) => n.to_string(),
835                _ => return Err(Raised::symbol("not_an_int")),
836            };
837            Ok(Value::Str(Text::from(s.as_str())))
838        }
839        // Inverse of `Int.str`: parse an integer literal (decimal / `0x`
840        // hex / `0b` binary, with optional `_` separators and leading sign)
841        // from a string. A non-string raises `.not_a_str`; a string that is
842        // not a valid literal or has trailing content raises `.cannot_parse_int`.
843        Builtin::IntParse => {
844            let s = str_ref(&args[0])?;
845            let n = crate::parse::parse_int(s).ok_or_else(|| Raised::symbol("cannot_parse_int"))?;
846            Ok(Value::from_bigint(n))
847        }
848        // Build a string from a list of **codepoints** (Unicode scalar values). A
849        // non-int element raises `.not_an_int`; a value outside `0..=0x10FFFF` or in
850        // the surrogate range raises `.bad_codepoint`.
851        Builtin::StrPack => {
852            let t = list(&args[0])?;
853            let mut s = String::new();
854            for elem in t.iter() {
855                let code = match elem {
856                    Value::I64(x) => u32::try_from(*x).ok(),
857                    Value::BigInt(n) => u32::try_from(&**n).ok(),
858                    _ => return Err(Raised::symbol("not_an_int")),
859                };
860                let cp = code
861                    .and_then(char::from_u32)
862                    .ok_or_else(|| Raised::symbol("bad_codepoint"))?;
863                s.push(cp);
864            }
865            Ok(Value::Str(Text::from(s.as_str())))
866        }
867        // Explode a string into a list of its codepoints — the inverse of
868        // `__Str.pack`. A non-string raises `.not_a_str`.
869        Builtin::StrUnpack => {
870            let mut t = Vector::new();
871            for ch in str_ref(&args[0])?.chars() {
872                t.push_back_mut(Value::I64(ch as i64));
873            }
874            Ok(Value::List(t))
875        }
876        // Intern a string as a symbol (`"foo"` → `.foo`); the string→symbol half of
877        // the text bridge. The text must be a single Muon `<sym>` segment — the
878        // only form that re-parses from `.<text>` — else `.bad_symbol`. A
879        // non-string raises `.not_a_str`.
880        Builtin::SymFrom => {
881            let s = str_ref(&args[0])?;
882            if is_sym_segment(s) {
883                Ok(Value::Symbol(s.clone()))
884            } else {
885                Err(Raised::symbol("bad_symbol"))
886            }
887        }
888        // A symbol's text without the dot (`.foo` → `"foo"`); the Sym half of the
889        // scalar→text bridge and the inverse of `__Sym.from`. A non-symbol raises
890        // `.not_a_sym`.
891        Builtin::SymStr => match &args[0] {
892            Value::Symbol(s) => Ok(Value::Str(s.clone())),
893            _ => Err(Raised::symbol("not_a_sym")),
894        },
895        // The canonical name a module's code came from (see [`ModuleData::name`]).
896        Builtin::ModName => match &args[0] {
897            Value::Module(node) => match &**node {
898                EnvNode::Module { data, .. } => Ok(match data.name() {
899                    Some(name) => Value::Str(name.clone()),
900                    // The host never named this code, so there is no name to
901                    // report and the unit says so.
902                    None => unit(),
903                }),
904                _ => Err(Raised::symbol("not_a_module")),
905            },
906            _ => Err(Raised::symbol("not_a_module")),
907        },
908        // The exported item names as a sorted list of symbols (see
909        // [`Builtin::ModExports`]). The table is name-sorted already, so the
910        // listing is a straight read — no sort, and no body evaluated.
911        Builtin::ModExports => match &args[0] {
912            Value::Module(node) => match &**node {
913                EnvNode::Module { data, .. } => {
914                    let mut out = Vector::new();
915                    for (name, _) in data.items() {
916                        out.push_back_mut(Value::Symbol(name.clone()));
917                    }
918                    Ok(Value::List(out))
919                }
920                _ => Err(Raised::symbol("not_a_module")),
921            },
922            _ => Err(Raised::symbol("not_a_module")),
923        },
924        // The prototype of any value at all (see [`proto_of`]).
925        Builtin::Proto => Ok(proto_of(&args[0], ctx)),
926        Builtin::Repr => {
927            let s = args[0].to_display();
928            Ok(Value::Str(Text::from(s.as_str())))
929        }
930        // `__new proto data` — an object of the prototype the leaf supplied. The
931        // prototype slot takes a module and nothing else, which is what keeps a
932        // prototype a const-time entity with a known item table.
933        Builtin::ObjNew => {
934            let Value::Module(proto) = &args[0] else {
935                return Err(Raised::symbol("not_a_module"));
936            };
937            Ok(Value::Object(Rc::new(ObjectData {
938                proto: proto.clone(),
939                data: args[1].clone(),
940            })))
941        }
942        // `__value proto obj` — the payload, provided the object is of that
943        // prototype. A non-object is `.not_an_object`; an object of another
944        // prototype is `.foreign_object`, so a method cannot unwrap a value that
945        // merely passed through it.
946        Builtin::ObjValue => {
947            let Value::Module(proto) = &args[0] else {
948                return Err(Raised::symbol("not_a_module"));
949            };
950            let Value::Object(obj) = &args[1] else {
951                return Err(Raised::symbol("not_an_object"));
952            };
953            if !EnvRef::ptr_eq(&obj.proto, proto) {
954                return Err(Raised::symbol("foreign_object"));
955            }
956            Ok(obj.data.clone())
957        }
958        // A builtin module names itself: 0-arity, so a reference *is* the module
959        // value (see [`Ctx::builtin_module`]).
960        Builtin::Int
961        | Builtin::Str
962        | Builtin::Sym
963        | Builtin::List
964        | Builtin::Map
965        | Builtin::Err
966        | Builtin::Mod
967        | Builtin::Bool
968        | Builtin::Fun
969        | Builtin::Unit => Ok(ctx.builtin_module(op)),
970    }
971}
972
973/// Borrows an integer as a `BigInt`, or raises `.not_an_int`. `BigInt` is
974/// borrowed; `I64` materializes a small allocation. Fast paths in `invoke` avoid this.
975fn as_big(v: &Value) -> Result<Cow<'_, BigInt>, Raised> {
976    match v {
977        Value::I64(x) => Ok(Cow::Owned(BigInt::from(*x))),
978        Value::BigInt(n) => Ok(Cow::Borrowed(n)),
979        _ => Err(Raised::symbol("not_an_int")),
980    }
981}
982
983/// Binary `+ - *` logic: `i64` fast path with `BigInt` fallback. The fallback
984/// borrows operands to minimize allocations, normalizing the result via
985/// [`Value::from_bigint`] to maintain the `I64`-if-it-fits invariant.
986fn int_arith(
987    a: &Value,
988    b: &Value,
989    small: impl Fn(i64, i64) -> Option<i64>,
990    big: impl Fn(&BigInt, &BigInt) -> BigInt,
991) -> Result<Value, Raised> {
992    if let (Value::I64(x), Value::I64(y)) = (a, b) {
993        if let Some(z) = small(*x, *y) {
994            return Ok(Value::I64(z));
995        }
996    }
997    Ok(Value::from_bigint(big(&*as_big(a)?, &*as_big(b)?)))
998}
999
1000/// Relational `__Int.*` logic: compares two integers and maps [`Ordering`] to a
1001/// [`Bool`](Builtin::Bool) iota. Uses `i64` fast path and `BigInt` fallback.
1002fn int_cmp(
1003    a: &Value,
1004    b: &Value,
1005    ctx: &Ctx,
1006    hold: impl Fn(Ordering) -> bool,
1007) -> Result<Value, Raised> {
1008    let ord = if let (Value::I64(x), Value::I64(y)) = (a, b) {
1009        x.cmp(y)
1010    } else {
1011        as_big(a)?.cmp(&as_big(b)?)
1012    };
1013    Ok(bool_value(ctx, hold(ord)))
1014}
1015
1016/// Borrows a value as a list, or raises `.not_a_list`. Used by `__List.*` builtins.
1017fn list(v: &Value) -> Result<&Vector<Value>, Raised> {
1018    match v {
1019        Value::List(t) => Ok(t),
1020        _ => Err(Raised::symbol("not_a_list")),
1021    }
1022}
1023
1024/// Borrows a value as a string, or raises `.not_a_str`. Used by `__Str.*` / `__Sym.*` builtins.
1025fn str_ref(v: &Value) -> Result<&Text, Raised> {
1026    match v {
1027        Value::Str(s) => Ok(s),
1028        _ => Err(Raised::symbol("not_a_str")),
1029    }
1030}
1031
1032/// Checks if `s` is a single Muon `<sym>` (non-empty run of `[0-9A-Za-z_+-]`).
1033/// This is the admissible domain of `__Sym.from`.
1034fn is_sym_segment(s: &str) -> bool {
1035    !s.is_empty()
1036        && s.bytes()
1037            .all(|c| c.is_ascii_alphanumeric() || matches!(c, b'_' | b'-' | b'+'))
1038}
1039
1040/// Borrows a value as a map, or raises `.not_a_map`. Used by `__Map.*` builtins.
1041fn map(v: &Value) -> Result<&HashTrieMap<Value, Value>, Raised> {
1042    match v {
1043        Value::Map(m) => Ok(m),
1044        _ => Err(Raised::symbol("not_a_map")),
1045    }
1046}
1047
1048/// Encodes an optional map value as `__Map.merge`'s option: `[]` (none) or `[v]`
1049/// (some) for the merge callback.
1050fn option(v: Option<&Value>) -> Value {
1051    match v {
1052        None => Value::List(Vector::new()),
1053        Some(v) => {
1054            let mut t = Vector::new();
1055            t.push_back_mut(v.clone());
1056            Value::List(t)
1057        }
1058    }
1059}
1060
1061/// Decodes a `__Map.merge` callback result as an option: `[]` (drop key) or `[v]`
1062/// (set key). Raises `.not_a_list` or `.list_size_mismatch`.
1063fn un_option(v: &Value) -> Result<Option<Value>, Raised> {
1064    let t = list(v)?;
1065    match t.len() {
1066        0 => Ok(None),
1067        1 => Ok(Some(t.get(0).unwrap().clone())),
1068        _ => Err(Raised::symbol("list_size_mismatch")),
1069    }
1070}
1071
1072/// Reads an integer as a 0-based index. Non-integers raise `.not_an_int`;
1073/// negative or out-of-`usize` values yield `None`. Shared by `__List.get`/`with`/`split`.
1074fn index_of(v: &Value) -> Result<Option<usize>, Raised> {
1075    match v {
1076        // A negative `i64` fails `usize::try_from` → `None` (out of range).
1077        Value::I64(x) => Ok(usize::try_from(*x).ok()),
1078        Value::BigInt(n) => Ok(u64::try_from(&**n)
1079            .ok()
1080            .and_then(|u| usize::try_from(u).ok())),
1081        _ => Err(Raised::symbol("not_an_int")),
1082    }
1083}
1084
1085/// Euclidean quotient/remainder: `a = b*q + r` with `0 <= r < |b|`.
1086/// Raises `.div_by_zero` if `b == 0`.
1087fn div_rem_euclid(a: &BigInt, b: &BigInt) -> Result<(BigInt, BigInt), Raised> {
1088    if b.sign() == Sign::NoSign {
1089        return Err(Raised::symbol("div_by_zero"));
1090    }
1091    let q = a / b; // truncated toward zero
1092    let r = a - &q * b;
1093    // Truncated `r` carries the sign of `a`; nudge it into `[0, |b|)`.
1094    Ok(if r.sign() == Sign::Minus {
1095        if b.sign() == Sign::Plus {
1096            (q - 1, r + b)
1097        } else {
1098            (q + 1, r - b)
1099        }
1100    } else {
1101        (q, r)
1102    })
1103}
1104
1105/// The unit value `()`. Fed by the nullary marker `()` and used to force the
1106/// `__Int.*` / `__Err.catch` branch thunks.
1107fn unit() -> Value {
1108    Value::Unit
1109}
1110
1111/// The `false` / `true` iota positions in [`__Bool`](Builtin::Bool)'s name-sorted
1112/// iota table — `false` before `true`. [`bool_value`] and [`as_bool`] both read
1113/// the table through these, so a change to the table's order is a change here.
1114const BOOL_FALSE_IDX: u32 = 0;
1115const BOOL_TRUE_IDX: u32 = 1;
1116
1117/// A [`Bool`](Builtin::Bool) iota for a Rust `bool`, derived from the context's
1118/// cached `__Bool` instance — no minting, exactly as a member access on the module
1119/// would produce (see the *iotas* section of
1120/// `docs/done/2026-08-13_elly-modules-iotas.md`). Both `__true` and `Bool.true`
1121/// funnel here, so they are the same iota of the same instance and compare equal.
1122fn bool_value(ctx: &Ctx, b: bool) -> Value {
1123    let Value::Module(home) = ctx.builtin_module(Builtin::Bool) else {
1124        unreachable!("a builtin module is a module value")
1125    };
1126    let index = if b { BOOL_TRUE_IDX } else { BOOL_FALSE_IDX };
1127    Value::Iota { home, index }
1128}
1129
1130/// Read a value as a Rust `bool`, or raise `.not_a_bool` — the strict-argument
1131/// check the `__Bool.*` logic shares, mirroring [`as_big`] / [`list`]. A value is
1132/// a boolean only if it is an iota of *this* context's `__Bool` instance, told by
1133/// pointer identity against the cached module; the index then names which.
1134pub(crate) fn as_bool(v: &Value, ctx: &Ctx) -> Result<bool, Raised> {
1135    let Value::Module(home) = ctx.builtin_module(Builtin::Bool) else {
1136        unreachable!("a builtin module is a module value")
1137    };
1138    if let Value::Iota { home: h, index } = v {
1139        if EnvRef::ptr_eq(h, &home) {
1140            return Ok(*index == BOOL_TRUE_IDX);
1141        }
1142    }
1143    Err(Raised::symbol("not_a_bool"))
1144}