Skip to main content

elly_core/eval/
mod.rs

1//! Naive tree-walking evaluator for the first Elly subset.
2//!
3//! Uses call-by-value evaluation. Environments are persistent linked lists of
4//! bindings shared via `Rc` for efficient closure capture. The tree it walks is
5//! a [`StaticChunk`](crate::code::StaticChunk): every node is read by its id
6//! through the realm's chunk table, and the chunk being run is passed along as
7//! `at`, the current-chunk register.
8//!
9//! What runs lives here, with the values and environments it builds: the
10//! environment chain (`env`), pattern matching (`patmatch`), running the
11//! builtins (`builtin`), and raised errors with what they leave as they
12//! unwind (`error`). The static surface of the language (code, parsing,
13//! modules, sources) is outside `eval/`.
14
15pub(crate) mod builtin;
16pub(crate) mod env;
17#[cfg(feature = "stats-env")]
18pub mod env_stats;
19pub(crate) mod error;
20mod patmatch;
21
22use alloc::boxed::Box;
23use alloc::rc::Rc;
24use alloc::vec::Vec;
25
26use rpds::{HashTrieMap, Vector};
27
28use crate::code::{ChunkId, LambdaId, ModId, Node, NodeId, Pat, PatId, Plan, Run};
29
30use crate::builtins::Builtin;
31use crate::eval::env::{Env, EnvNode, EnvRef};
32use crate::eval::error::Raised;
33use crate::eval::patmatch::{match_pat, match_pattern};
34use crate::sym::{Projection, Sym};
35use crate::value::Value;
36use crate::Ctx;
37
38// ---------------------------------------------------------------------------
39// The realm's run-time state
40// ---------------------------------------------------------------------------
41
42impl Ctx {
43    /// Mints the next unique id for a closure's identity, used for equality
44    /// and ordering.
45    pub(crate) fn next_id(&self) -> u64 {
46        let n = self.ids.get();
47        self.ids.set(n + 1);
48        n
49    }
50
51    /// Reserves a block of `n` consecutive ids and returns the base. Used by
52    /// [`instantiate`] to give module items stable identities, preventing alias
53    /// with individually minted ids.
54    pub(crate) fn reserve_ids(&self, n: u64) -> u64 {
55        let base = self.ids.get();
56        self.ids.set(base + n);
57        base
58    }
59
60    /// Gets a builtin module (`__Int`, `__Map`, etc.), instantiated on first
61    /// reference and cached.
62    ///
63    /// Builtin modules live in the [`ELLY`](ChunkId::ELLY) chunk, with builtin
64    /// leaves as item bodies. A written `__Int.add` is folded to its member at
65    /// resolve, but this method handles remaining dynamic uses of the module as
66    /// a value.
67    pub(crate) fn builtin_module(&self, op: Builtin) -> Value {
68        let slot = op.module_slot().expect("a builtin module");
69        self.modules[slot]
70            .get_or_init(|| {
71                instantiate_module(ChunkId::ELLY, ModId::builtin(slot), &[], self)
72                    .expect("a builtin module has no frame")
73            })
74            .clone()
75    }
76}
77
78/// Evaluates the `__main` body of a module instance in its own environment.
79/// Returns `Ok(None)` if no body is declared (e.g., a library).
80///
81/// This implements "runnable" module semantics: [`crate::load_module`] instantiates
82/// modules without evaluating their bodies, ensuring `__main` is only executed
83/// when explicitly targeted by a runner.
84///
85/// The body executes in the instance's [`Module`](EnvNode::Module) terminal.
86/// As it is not a member, it has no reserved ID. Non-module instances raise
87/// `.not_a_module`. Result interpretation is handled by the runner; see
88/// `docs/done/2026-08-14_elly-run.md`.
89pub fn eval_main(instance: &Value, ctx: &Ctx) -> Result<Option<Value>, Raised> {
90    let Value::Module(home) = instance else {
91        return Err(Raised::symbol(Sym::NOT_A_MODULE));
92    };
93    let EnvNode::Module { chunk, module, .. } = **home else {
94        unreachable!("a module value always holds a module terminal")
95    };
96    let body = ctx.chunk(chunk).module(module).body;
97    let Some(body) = body.get() else {
98        return Ok(None);
99    };
100    ctx.clear_unwind();
101    let result = eval(chunk, body, &Env::from_ref(home.clone()), ctx)?;
102    Ok(Some(result))
103}
104
105/// Apply a value to a single argument in the realm `ctx` — the public entry
106/// point behind a host's "call this value" surface (e.g. the Python bindings'
107/// `elly.Fun.__call__`).
108///
109/// This is the same application step the evaluator uses internally (`apply_n`
110/// with one argument): a closure call, list projection, or one step toward
111/// saturating a builtin. `f` and `arg` must belong to `ctx`; closures minted by
112/// the call continue its id source.
113pub fn apply_with(f: &Value, arg: Value, ctx: &Ctx) -> Result<Value, Raised> {
114    ctx.clear_unwind();
115    apply1(f.clone(), arg, ctx)
116}
117
118/// Runs an expression chunk (see [`Ctx::parse`]) in `env`: its top module's
119/// body, with no module terminal around it. Public entry point for host
120/// evaluation (e.g., `elly.Env.eval`).
121pub fn eval_env(chunk: ChunkId, env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
122    let body = {
123        let code = ctx.chunk(chunk);
124        let top = code.module(code.top());
125        assert!(
126            top.members.len == 0 && top.frame.len == 0,
127            "only an expression chunk runs on its own"
128        );
129        top.body
130    };
131    ctx.clear_unwind();
132    eval(chunk, body, env, ctx)
133}
134
135/// Evaluates node `id` of chunk `at` in `env`.
136pub(crate) fn eval(at: ChunkId, id: NodeId, env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
137    match ctx.node(at, id) {
138        // Resolved reference: look up value by index.
139        Node::Local { index, .. } => Ok(env.lookup(index)),
140        // Module sibling reference (see [`mod_item`]).
141        Node::ModItem { index, depth, .. } => mod_item(env, index, depth, ctx),
142        // Home-module leaf (see [`home_leaf`]).
143        Node::Home { leaf, depth } => env.home_leaf(leaf, depth),
144        // Recursive binding group (see [`recur`]).
145        Node::Recur(group) => recur(at, group, env, ctx),
146        // Block evaluation (see [`eval_block`]). Outlined to optimize I-cache.
147        Node::Block { len, binds, exprs } => eval_block(at, len, binds, exprs, env, ctx),
148        // Builtin operation. 0-arity builtins execute immediately; others return
149        // a `Builtin` awaiting arguments.
150        Node::Builtin(op) => {
151            if op.arity() == 0 {
152                builtin::invoke(op, &[], ctx)
153            } else {
154                // Avoid `with_capacity` to prevent unnecessary allocations during
155                // curried application cloning.
156                Ok(Value::Builtin {
157                    op,
158                    args: Vec::new(),
159                })
160            }
161        }
162        Node::Int(n) => Ok(Value::I64(n)),
163        // A chunk keeps only the integers that do not fit `i64` as big ones.
164        Node::BigInt(n) => Ok(Value::BigInt(Rc::new(ctx.chunk(at).int(n).clone()))),
165        Node::Symbol(s) => Ok(Value::Symbol(s)),
166        // String handles are shared and pre-decoded.
167        Node::Str(s) => Ok(Value::Str(ctx.chunk(at).str(s).clone())),
168        Node::Unit => Ok(Value::Unit),
169        Node::List { len, elems } => {
170            ctx.check_run(at, Run::List, elems, len);
171            let mut vs = Vector::new();
172            for i in 0..len {
173                let elem = eval(at, elems.nth(i), env, ctx)?;
174                vs.push_back_mut(elem);
175            }
176            Ok(Value::List(vs))
177        }
178        // Map literal: evaluate keys and values in source order. Later entries
179        // with the same key overwrite previous ones.
180        Node::Map { len, entries } => {
181            ctx.check_run(at, Run::Map, entries, 2 * len);
182            let mut m = HashTrieMap::new();
183            for i in 0..len {
184                let kv = eval(at, entries.nth(i), env, ctx)?;
185                let vv = eval(at, entries.nth(len + i), env, ctx)?;
186                m.insert_mut(kv, vv);
187            }
188            Ok(Value::Map(m))
189        }
190        // Closure creation: captures free variables according to the resolution plan.
191        Node::Abs(code) => Ok(closure(at, code, ctx.next_id(), env, ctx)),
192        // Call-by-value application. Callee and arguments are evaluated strictly
193        // left-to-right. Small arities (≤ 4) are handled on the stack to avoid
194        // heap allocation for the argument buffer.
195        Node::App { len, args } => {
196            ctx.check_run(at, Run::App, args, len);
197            let callee = eval(at, args, env, ctx)?;
198            // The call passes itself down as the site an error from the call
199            // itself, not from evaluating its operands, unwinds through: the
200            // callee records it, so that this stays a tail call.
201            match len - 1 {
202                1 => {
203                    let arg = eval(at, args.nth(1), env, ctx)?;
204                    apply1_at(callee, arg, at, id, ctx)
205                }
206                2 => {
207                    let va = eval(at, args.nth(1), env, ctx)?;
208                    let vb = eval(at, args.nth(2), env, ctx)?;
209                    apply_n_at(callee, &[va, vb], at, id, ctx)
210                }
211                3 => {
212                    let va = eval(at, args.nth(1), env, ctx)?;
213                    let vb = eval(at, args.nth(2), env, ctx)?;
214                    let vc = eval(at, args.nth(3), env, ctx)?;
215                    apply_n_at(callee, &[va, vb, vc], at, id, ctx)
216                }
217                4 => {
218                    let va = eval(at, args.nth(1), env, ctx)?;
219                    let vb = eval(at, args.nth(2), env, ctx)?;
220                    let vc = eval(at, args.nth(3), env, ctx)?;
221                    let vd = eval(at, args.nth(4), env, ctx)?;
222                    apply_n_at(callee, &[va, vb, vc, vd], at, id, ctx)
223                }
224                n => {
225                    let mut argv: Vec<Value> = Vec::with_capacity(n as usize);
226                    for i in 1..len {
227                        argv.push(eval(at, args.nth(i), env, ctx)?);
228                    }
229                    apply_n_at(callee, &argv, at, id, ctx)
230                }
231            }
232        }
233        // Subject is evaluated once, then arms are tried in order. It comes
234        // after the arm bodies in the run.
235        Node::Case { len, conds, exprs } => {
236            ctx.check_run(at, Run::Case, exprs, len + 1);
237            let subj = eval(at, exprs.nth(len), env, ctx)?;
238            run_arms(at, &subj, len, conds, exprs, env, ctx)
239        }
240        #[cfg(debug_assertions)]
241        Node::Barrier { .. } => unreachable!("a barrier is not an operand"),
242    }
243}
244
245/// A closure over lambda `code` of chunk `at`, with identity `id`: captures
246/// free variables according to the resolution plan. `Plan::Chain` shares the
247/// environment; `Plan::Frame` creates a new frame.
248#[inline]
249fn closure(at: ChunkId, code: LambdaId, id: u64, env: &Env, ctx: &Ctx) -> Value {
250    let lambda = ctx.lambda(at, code);
251    let env = match lambda.captures {
252        Plan::Chain => env.clone(),
253        // Capturing clones values and allocates cells but runs no code, so it
254        // cannot add a chunk while the plan is borrowed.
255        Plan::Frame(plan) => env.capture_env(ctx.chunk(at).captures(plan), ctx),
256    };
257    Value::Closure {
258        id,
259        chunk: at,
260        lambda: code,
261        applied: 0,
262        arity: lambda.head.len as u8,
263        env,
264    }
265}
266
267/// Tries case arms in order. `NoMatch` refutations fall through; other errors propagate.
268/// If no arms match, `.no_match` is raised.
269#[inline(never)]
270fn run_arms(
271    at: ChunkId,
272    subj: &Value,
273    len: u32,
274    conds: PatId,
275    bodies: NodeId,
276    env: &Env,
277    ctx: &Ctx,
278) -> Result<Value, Raised> {
279    ctx.check_pat_run(at, Run::Case, conds, len);
280    for i in 0..len {
281        match match_pattern(at, conds.nth(i), subj, env.clone(), ctx) {
282            // Not released explicitly: only 0.05% of node deaths happen here, and
283            // holding the result across a release grows this recursive frame.
284            Ok(arm_env) => {
285                // What the arms before this one refuted explains nothing now.
286                if i > 0 {
287                    ctx.caught();
288                }
289                return eval(at, bodies.nth(i), &arm_env, ctx);
290            }
291            Err(Raised::NoMatch(_)) => ctx.caught(),
292            Err(e) => return Err(e),
293        }
294    }
295    Err(ctx.no_arm_matched(at, bodies.nth(len), subj))
296}
297
298/// Specialized path for single-argument application. Used by the call spine,
299/// fold builtins, and match clauses. Binds one parameter, produces a partial
300/// closure, gathers a builtin argument, or projects a list.
301#[inline(always)]
302pub(crate) fn apply1(fv: Value, av: Value, ctx: &Ctx) -> Result<Value, Raised> {
303    apply1_at(fv, av, ChunkId::NONE, NodeId::NONE, ctx)
304}
305
306/// [`apply1`] for application node `call` of chunk `at`, which an error from
307/// the call unwinds through, or [`NodeId::NONE`] for an application with no
308/// node, such as a builtin's callback.
309fn apply1_at(fv: Value, av: Value, at: ChunkId, call: NodeId, ctx: &Ctx) -> Result<Value, Raised> {
310    match fv {
311        Value::Closure {
312            chunk,
313            lambda,
314            applied,
315            arity,
316            env,
317            ..
318        } => {
319            let code = ctx.lambda(chunk, lambda);
320            ctx.check_pat_run(chunk, Run::Head, code.head.first, code.head.len);
321            // Bind argument and extend environment.
322            let pat = code.head.first.nth(u32::from(applied));
323            let call_env = match match_pattern(chunk, pat, &av, env, ctx) {
324                Ok(env) => env,
325                Err(r) => {
326                    let pat = code.head.first.nth(u32::from(applied));
327                    return Err(unbound(r, chunk, pat, &av, at, call, ctx));
328                }
329            };
330            if applied + 1 < arity {
331                // Return partial closure with updated applied count and environment.
332                return Ok(Value::Closure {
333                    id: ctx.next_id(),
334                    chunk,
335                    lambda,
336                    applied: applied + 1,
337                    arity,
338                    env: call_env,
339                });
340            }
341            let result = eval(chunk, code.body, &call_env, ctx);
342            ctx.release(call_env);
343            traced(result, at, call, ctx)
344        }
345        Value::Builtin {
346            op,
347            args: mut gathered,
348        } => {
349            // Unary builtins saturate from the stack.
350            if gathered.is_empty() && op.arity() == 1 {
351                return traced(builtin::invoke(op, &[av], ctx), at, call, ctx);
352            }
353            gathered.push(av);
354            if gathered.len() == op.arity() {
355                traced(builtin::invoke(op, &gathered, ctx), at, call, ctx)
356            } else {
357                Ok(Value::Builtin { op, args: gathered })
358            }
359        }
360        Value::HostFn {
361            code,
362            args: gathered,
363        } => {
364            if gathered.is_empty() && code.arity == 1 {
365                return traced((code.f)(&[av], ctx), at, call, ctx);
366            }
367            let gathered = extended(&gathered, &[av]);
368            if gathered.len() == code.arity {
369                traced((code.f)(&gathered, ctx), at, call, ctx)
370            } else {
371                Ok(Value::HostFn {
372                    code,
373                    args: gathered,
374                })
375            }
376        }
377        // Materialize item when a module is applied to a symbol.
378        Value::Module(home) => {
379            let result = match av {
380                Value::Symbol(s) => materialize(&Env::from_ref(home), s, ctx),
381                _ => Err(Raised::symbol(Sym::NOT_APPLICABLE)),
382            };
383            traced(result, at, call, ctx)
384        }
385        // Dispatch via prototype for all other types.
386        fv => traced(receiver(fv, &av, &[], ctx), at, call, ctx),
387    }
388}
389
390/// The error from matching `arg` against parameter `pat` of chunk `chunk`,
391/// recorded as unwinding through application node `call` of chunk `at`. A
392/// refutation escapes the parameter there, and says so if the call has a
393/// node: a builtin applying a callback, as `__match` does its clauses, may
394/// catch it.
395#[cold]
396#[inline(never)]
397fn unbound(
398    raised: Raised,
399    chunk: ChunkId,
400    pat: PatId,
401    arg: &Value,
402    at: ChunkId,
403    call: NodeId,
404    ctx: &Ctx,
405) -> Raised {
406    if call == NodeId::NONE {
407        return raised;
408    }
409    ctx.unwound(at, call);
410    ctx.escaped(raised, chunk, pat, arg)
411}
412
413/// `result`, recording an error as unwinding through application node `call`
414/// of chunk `at`.
415#[inline(always)]
416fn traced(
417    result: Result<Value, Raised>,
418    at: ChunkId,
419    call: NodeId,
420    ctx: &Ctx,
421) -> Result<Value, Raised> {
422    if result.is_err() {
423        ctx.unwound(at, call);
424    }
425    result
426}
427
428/// Applies a value to multiple arguments. Handles closures (multi-parameter
429/// matching), builtins (gather until saturation), and list projections.
430/// Over-application results loop onto the returned value.
431///
432/// Arguments are evaluated strictly left-to-right before matching patterns
433/// left-to-right, ensuring evaluation errors surface before pattern refutations.
434/// See `docs/done/2026-08-02_perf-less-currying.md` §3.
435#[inline(always)]
436fn apply_n(fv: Value, args: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
437    apply_n_at(fv, args, ChunkId::NONE, NodeId::NONE, ctx)
438}
439
440/// [`apply_n`] for application node `call` of chunk `at` (see
441/// [`apply1_at`]).
442fn apply_n_at(
443    mut fv: Value,
444    mut args: &[Value],
445    at: ChunkId,
446    call: NodeId,
447    ctx: &Ctx,
448) -> Result<Value, Raised> {
449    while !args.is_empty() {
450        match fv {
451            // Matches arguments against remaining patterns left-to-right.
452            // If arguments are exhausted, returns a partial closure with a fresh ID.
453            // Otherwise, executes the body and loops leftover arguments onto the result.
454            // Pattern misses raise `NoMatch`.
455            Value::Closure {
456                chunk,
457                lambda,
458                applied,
459                arity,
460                env,
461                ..
462            } => {
463                let code = ctx.lambda(chunk, lambda);
464                ctx.check_pat_run(chunk, Run::Head, code.head.first, code.head.len);
465                let remaining = usize::from(arity - applied);
466                let take = remaining.min(args.len());
467                // Thread the env through each parameter match, consing bindings
468                // onto the closure's captured (or partially-applied) env.
469                let mut call_env = env;
470                let first = code.head.first.nth(u32::from(applied));
471                for (i, arg) in args[..take].iter().enumerate() {
472                    let matched = match_pattern(chunk, first.nth(i as u32), arg, call_env, ctx);
473                    call_env = match matched {
474                        Ok(env) => env,
475                        Err(r) => {
476                            let pat = first.nth(i as u32);
477                            return Err(unbound(r, chunk, pat, arg, at, call, ctx));
478                        }
479                    };
480                }
481                if take < remaining {
482                    // Partial: no body runs yet; mint a fresh identity.
483                    return Ok(Value::Closure {
484                        id: ctx.next_id(),
485                        chunk,
486                        lambda,
487                        applied: applied + take as u8,
488                        arity,
489                        env: call_env,
490                    });
491                }
492                let result = eval(chunk, code.body, &call_env, ctx);
493                ctx.release(call_env);
494                fv = traced(result, at, call, ctx)?;
495                args = &args[take..];
496            }
497            // Gathers arguments and invokes once saturated, looping leftovers onto the result.
498            // If the argument spine satisfies the arity immediately, it invokes directly
499            // from the slice to avoid `Value::Builtin` allocation.
500            Value::Builtin {
501                op,
502                args: mut gathered,
503            } => {
504                let arity = op.arity();
505                if gathered.is_empty() && args.len() >= arity {
506                    fv = traced(builtin::invoke(op, &args[..arity], ctx), at, call, ctx)?;
507                    args = &args[arity..];
508                } else {
509                    let take = (arity - gathered.len()).min(args.len());
510                    gathered.extend(args[..take].iter().cloned());
511                    if gathered.len() < arity {
512                        return Ok(Value::Builtin { op, args: gathered });
513                    }
514                    fv = traced(builtin::invoke(op, &gathered, ctx), at, call, ctx)?;
515                    args = &args[take..];
516                }
517            }
518            // Similar gather-and-fire loop as builtins, dispatching via the host callback.
519            Value::HostFn {
520                code,
521                args: gathered,
522            } => {
523                let arity = code.arity;
524                if gathered.is_empty() && args.len() >= arity {
525                    fv = traced((code.f)(&args[..arity], ctx), at, call, ctx)?;
526                    args = &args[arity..];
527                } else {
528                    let take = (arity - gathered.len()).min(args.len());
529                    let gathered = extended(&gathered, &args[..take]);
530                    if gathered.len() < arity {
531                        return Ok(Value::HostFn {
532                            code,
533                            args: gathered,
534                        });
535                    }
536                    fv = traced((code.f)(&gathered, ctx), at, call, ctx)?;
537                    args = &args[take..];
538                }
539            }
540            // Materializes an item when applied to a symbol; leftovers loop onto the result.
541            Value::Module(home) => {
542                let result = match &args[0] {
543                    Value::Symbol(s) => materialize(&Env::from_ref(home), *s, ctx),
544                    _ => Err(Raised::symbol(Sym::NOT_APPLICABLE)),
545                };
546                fv = traced(result, at, call, ctx)?;
547                args = &args[1..];
548            }
549            // Dispatches by passing the entire remaining argument spine to the
550            // method in one call (see [`receiver`]).
551            fv => return traced(receiver(fv, &args[0], &args[1..], ctx), at, call, ctx),
552        }
553    }
554    Ok(fv)
555}
556
557/// Concatenates `prefix` and `more` into a single allocation.
558/// Used by [`Value::HostFn`] for efficient argument gathering during partial application.
559fn extended(prefix: &[Value], more: &[Value]) -> Box<[Value]> {
560    let mut all = Vec::with_capacity(prefix.len() + more.len());
561    all.extend_from_slice(prefix);
562    all.extend_from_slice(more);
563    all.into_boxed_slice()
564}
565
566/// Returns the prototype of a value. Objects use their own prototype; others
567/// use the corresponding builtin module, providing a nominal type for all values.
568///
569/// Iotas resolve to their declaring module, allowing them to be treated as
570/// objects of that module. Callables (closures, builtins, host functions)
571/// resolve to `__Fun`.
572pub(crate) fn proto_of(v: &Value, ctx: &Ctx) -> Value {
573    match v {
574        Value::Object(obj) => obj.proto(),
575        Value::Iota { home, .. } => Value::Module(home.clone()),
576        Value::I64(_) | Value::BigInt(_) => ctx.builtin_module(Builtin::Int),
577        Value::Symbol(_) => ctx.builtin_module(Builtin::Sym),
578        Value::Str(_) => ctx.builtin_module(Builtin::Str),
579        Value::Unit => ctx.builtin_module(Builtin::Unit),
580        Value::List(_) => ctx.builtin_module(Builtin::List),
581        Value::Map(_) => ctx.builtin_module(Builtin::Map),
582        Value::Closure { .. } | Value::Builtin { .. } | Value::HostFn { .. } => {
583            ctx.builtin_module(Builtin::Fun)
584        }
585        Value::Module(_) => ctx.builtin_module(Builtin::Mod),
586    }
587}
588
589/// Whether `value`'s prototype is the module node `expr` of chunk `at` names —
590/// the general form of an
591/// `as <Proto>` pattern, where [`ProtoRef::Kind`] is the compiled one. A reference
592/// that is not a module raises `.not_a_module`: it is a mistake about the pattern
593/// rather than about the subject, so it is a real error and not a refutation.
594///
595/// Outlined for the reason [`receiver`] is: `match_pattern` is on every call's
596/// path, and its arms pay for each other's code size.
597#[inline(never)]
598pub(crate) fn proto_is(
599    at: ChunkId,
600    expr: NodeId,
601    value: &Value,
602    env: &Env,
603    ctx: &Ctx,
604) -> Result<bool, Raised> {
605    let expected = eval(at, expr, env, ctx)?;
606    if !matches!(expected, Value::Module(_)) {
607        return Err(Raised::symbol(Sym::NOT_A_MODULE));
608    }
609    Ok(proto_of(value, ctx) == expected)
610}
611
612/// Resolves a module sibling reference by walking to the home module and
613/// materializing the item at `index`.
614///
615/// The index is assigned during resolution against the module's sorted table,
616/// enabling direct slice access. Only module-item bodies contain `ModItem`,
617/// and they always run under a `Module` home terminal, which is found by
618/// walking through captured frames.
619///
620/// Outlined to maintain I-cache residency of `eval_env`.
621///
622/// If the index is past the item table, it is treated as an iota, which is
623/// derived as a pair of the terminal and position.
624///
625/// `depth` specifies how many terminals to skip to find the owning module.
626/// A `recur` group nested in a module body adds a terminal to the chain, requiring
627/// a skip for the enclosing module's items.
628#[inline(never)]
629fn mod_item(env: &Env, index: u32, depth: u32, ctx: &Ctx) -> Result<Value, Raised> {
630    let (env, home, (chunk, module, id_base)) = env
631        .find_module(depth)
632        .ok_or_else(|| Raised::symbol(Sym::NO_MODULE))?;
633    let member = ctx.chunk(chunk).member(module, index);
634    let Some(member) = member else {
635        // Handle iotas: derived from terminal identity and index.
636        let code = ctx.chunk(chunk);
637        let index = index - code.member_len(module);
638        if code.iota(module, index).is_none() {
639            return Err(Raised::symbol(Sym::MISSING_PROPERTY));
640        }
641        return Ok(Value::Iota {
642            home: home.clone(),
643            index,
644        });
645    };
646    materialize_body(chunk, member.node, env, id_base, index, ctx)
647}
648
649/// Evaluates a `recur` group by creating a `Module` terminal over the
650/// current environment and executing the body.
651///
652/// This is instantiation with a cons-shaped frame, avoiding copies by
653/// sharing the lexical environment. ID blocks are reserved per evaluation,
654/// ensuring different activations of the same `recur` group have distinct
655/// item identities. Bindings are materialized on reference.
656#[inline(never)]
657fn recur(at: ChunkId, group: ModId, env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
658    let (len, body) = {
659        let code = ctx.chunk(at);
660        (code.member_len(group), code.module(group).body)
661    };
662    let home = EnvRef::new(EnvNode::Module {
663        chunk: at,
664        module: group,
665        id_base: ctx.reserve_ids(u64::from(len)),
666        frame: env.clone(),
667    });
668    eval(at, body, &Env::from_ref(home), ctx)
669}
670
671/// Evaluates a block by threading the environment through clauses.
672/// Bindings extend the environment for subsequent clauses; other clauses
673/// execute for effect. The value of the final clause is returned.
674fn eval_block(
675    at: ChunkId,
676    len: u32,
677    binds: PatId,
678    exprs: NodeId,
679    env: &Env,
680    ctx: &Ctx,
681) -> Result<Value, Raised> {
682    let Some(last) = len.checked_sub(1) else {
683        return Ok(Value::Unit);
684    };
685    ctx.check_pat_run(at, Run::Block, binds, len);
686    ctx.check_run(at, Run::Block, exprs, len);
687    let mut env = env.clone();
688    for i in 0..last {
689        match ctx.pat(at, binds.nth(i)) {
690            Pat::None => {
691                eval(at, exprs.nth(i), &env, ctx)?;
692            }
693            pat => {
694                let v = eval(at, exprs.nth(i), &env, ctx)?;
695                env = match_pat(at, pat, &v, env, ctx)
696                    .map_err(|r| ctx.escaped(r, at, binds.nth(i), &v))?;
697            }
698        }
699    }
700    match ctx.pat(at, binds.nth(last)) {
701        Pat::None => {
702            let result = eval(at, exprs.nth(last), &env, ctx);
703            ctx.release(env);
704            result
705        }
706        pat => {
707            let v = eval(at, exprs.nth(last), &env, ctx)?;
708            let env = match_pat(at, pat, &v, env, ctx)
709                .map_err(|r| ctx.escaped(r, at, binds.nth(last), &v))?;
710            ctx.release(env);
711            Ok(v)
712        }
713    }
714}
715
716/// Instantiates a compiled module, the top module of `chunk` (see
717/// [`Ctx::compile_module`]), giving it its identity.
718///
719/// `frame` provides outer values against the module's frame names. A length
720/// mismatch raises `.module_arity`.
721pub fn instantiate(chunk: ChunkId, frame: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
722    let top = ctx.chunk(chunk).top();
723    instantiate_module(chunk, top, frame, ctx)
724}
725
726/// Instantiates module `module` of chunk `chunk`, creating its
727/// [`Module`](EnvNode::Module) terminal and returning it as a [`Value::Module`].
728///
729/// This assigns the module an identity (fresh `Rc`) and reserves its ID block
730/// so items can be stamped with stable IDs (see `materialize_body`).
731///
732/// `frame` provides outer values against the module's frame names. A length
733/// mismatch raises `.module_arity`. The frame is stored as a flat
734/// [`Frame`](EnvNode::Frame), though `recur` builds the terminal directly.
735pub(crate) fn instantiate_module(
736    chunk: ChunkId,
737    module: ModId,
738    frame: &[Value],
739    ctx: &Ctx,
740) -> Result<Value, Raised> {
741    let (frame_len, len) = {
742        let code = ctx.chunk(chunk);
743        (code.module(module).frame.len, code.member_len(module))
744    };
745    if frame.len() != frame_len as usize {
746        return Err(Raised::symbol(Sym::MODULE_ARITY));
747    }
748    let id_base = ctx.reserve_ids(u64::from(len));
749    let frame = if frame.is_empty() {
750        Env::EMPTY
751    } else {
752        Env::EMPTY.frame(Box::from(frame))
753    };
754    Ok(Value::Module(EnvRef::new(EnvNode::Module {
755        chunk,
756        module,
757        id_base,
758        frame,
759    })))
760}
761
762/// Evaluates an item's body, node `body` of `chunk`, in its module terminal.
763/// This is the common path for [`mod_item`] and [`materialize`].
764///
765/// Literal abstraction bodies are built directly with the item's reserved
766/// ID (`id_base + index`), ensuring multiple accesses produce equal closures.
767/// This avoids `eval` dispatch and `Ctx` counter writes for performance.
768///
769/// Only function definitions claim the item's identity. Forwarded callables
770/// retain their original identity, while data items or nested closures are
771/// processed via `eval`.
772#[inline]
773fn materialize_body(
774    chunk: ChunkId,
775    body: NodeId,
776    env: &Env,
777    id_base: u64,
778    index: u32,
779    ctx: &Ctx,
780) -> Result<Value, Raised> {
781    if let Node::Abs(code) = ctx.node(chunk, body) {
782        return Ok(closure(chunk, code, id_base + u64::from(index), env, ctx));
783    }
784    eval(chunk, body, env, ctx)
785}
786
787/// Materializes the item named `name` from the module object `home`.
788/// Binary-searches for the index and evaluates the body in the terminal.
789/// Uses the object's own node as the terminal to avoid environment allocation.
790/// Raises `.missing_property` if the name is absent.
791fn materialize(env: &Env, name: Sym, ctx: &Ctx) -> Result<Value, Raised> {
792    let (chunk, module, id_base) = env
793        .as_module()
794        .expect("a module object always holds a module terminal");
795    let member = {
796        let code = ctx.chunk(chunk);
797        code.index_of(module, name)
798            .map(|index| (index, code.member(module, index)))
799    };
800    let Some((index, Some(member))) = member else {
801        return Err(Raised::symbol(Sym::MISSING_PROPERTY));
802    };
803    materialize_body(chunk, member.node, env, id_base, index, ctx)
804}
805
806/// Applies a receiver to a symbol. This is the dispatch path for all non-callable,
807/// non-module values. The symbol identifies a member of the receiver's prototype,
808/// and the receiver is passed as an argument. [`proto_of`] ensures this is total.
809///
810/// List projection takes precedence: numeric symbols on lists project indices.
811/// Other symbols dispatch to `__List`.
812///
813/// Non-symbol arguments raise `.not_applicable`.
814///
815/// Outlined to maintain I-cache residency in `apply1` and `apply_n`.
816#[inline(never)]
817fn receiver(this: Value, name: &Value, rest: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
818    let Value::Symbol(s) = name else {
819        return Err(Raised::symbol(Sym::NOT_APPLICABLE));
820    };
821    let s = *s;
822    if let Value::List(elems) = &this {
823        let index = match ctx.projection(s) {
824            Projection::Name => None,
825            Projection::Index(i) => Some(i),
826            Projection::TooLarge => return Err(Raised::symbol(Sym::BAD_PROJECTION)),
827        };
828        if let Some(index) = index {
829            let projected = elems
830                .get(index as usize)
831                .cloned()
832                .ok_or_else(|| Raised::symbol(Sym::PROJECTION_OUT_OF_RANGE))?;
833            return apply_n(projected, rest, ctx);
834        }
835    }
836    let proto = if let Value::Object(obj) = &this {
837        obj.proto.clone()
838    } else if let Value::Module(home) = proto_of(&this, ctx) {
839        home
840    } else {
841        unreachable!("a prototype is always a module value")
842    };
843    dispatch(proto, this, s, rest, ctx)
844}
845
846/// Dispatches a method by materializing `name` from `proto` and applying it
847/// to the receiver and any remaining arguments.
848///
849/// Example: `x .m a b` is equivalent to `(materialize (proto of x) .m) x a b`.
850///
851/// The lookup is a binary search over the item table, ensuring locals are private.
852/// Arguments are handled similarly to the call spine in `eval_env`, with small
853/// arities processed on the stack.
854fn dispatch(
855    proto: EnvRef,
856    this: Value,
857    name: Sym,
858    rest: &[Value],
859    ctx: &Ctx,
860) -> Result<Value, Raised> {
861    let method = materialize(&Env::from_ref(proto), name, ctx)?;
862    match rest {
863        [] => apply1(method, this, ctx),
864        [a] => apply_n(method, &[this, a.clone()], ctx),
865        [a, b] => apply_n(method, &[this, a.clone(), b.clone()], ctx),
866        [a, b, c] => apply_n(method, &[this, a.clone(), b.clone(), c.clone()], ctx),
867        _ => {
868            let mut all: Vec<Value> = Vec::with_capacity(rest.len() + 1);
869            all.push(this);
870            all.extend_from_slice(rest);
871            apply_n(method, &all, ctx)
872        }
873    }
874}