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}