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}