elly_core/eval.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.
5
6use alloc::boxed::Box;
7use alloc::rc::Rc;
8use alloc::vec::Vec;
9
10use rpds::{HashTrieMap, Vector};
11
12use crate::ast::{recur_body, Captures, Expr, ModuleData, Pattern, Text};
13use crate::builtins;
14
15pub use crate::builtins::Builtin;
16pub use crate::eval_env::{Ctx, Env, EnvNode, EnvRef};
17use crate::eval_match::match_pattern;
18pub use crate::value::{HostCall, HostFn, ObjectData, Value};
19
20/// Evaluates the `__main` body of a module instance in its own environment.
21/// Returns `Ok(None)` if no body is declared (e.g., a library).
22///
23/// This implements "runnable" module semantics: [`crate::load_module`] instantiates
24/// modules without evaluating their bodies, ensuring `__main` is only executed
25/// when explicitly targeted by a runner.
26///
27/// The body executes in the instance's [`Module`](EnvNode::Module) terminal.
28/// As it is not a member, it has no reserved ID. Non-module instances raise
29/// `.not_a_module`. Result interpretation is handled by the runner; see
30/// `docs/done/2026-08-14_elly-run.md`.
31pub fn eval_main(instance: &Value, ctx: &Ctx) -> Result<Option<Value>, Raised> {
32 let Value::Module(home) = instance else {
33 return Err(Raised::symbol("not_a_module"));
34 };
35 let EnvNode::Module { data, .. } = &**home else {
36 unreachable!("a module value always holds a module terminal")
37 };
38 let Some(body) = data.body() else {
39 return Ok(None);
40 };
41 let result = eval_env(body, &Env::from_ref(home.clone()), ctx)?;
42 Ok(Some(result))
43}
44
45/// Value unwinding through the single error channel (see `docs/elly-spec.md`).
46///
47/// - `Error(v)`: Genuine failure (host abort or `__Err.raise v`). Propagates
48/// past `__match`.
49/// - `NoMatch(v)`: Pattern refutation from `match_pattern`. Only `__match` catches
50/// this, falling through to its fallback. Other boundaries treat it as `Error(v)`,
51/// surfacing the original error `v`.
52///
53/// Kinds are distinguished by construction; user code can only raise `Error`,
54/// preventing forged `NoMatch` signals.
55#[derive(Debug, Clone)]
56pub enum Raised {
57 /// A genuine error, propagated by `__match`.
58 Error(Value),
59 /// A pattern refutation carrying the original error it decays to when uncaught.
60 NoMatch(Value),
61}
62
63impl Raised {
64 /// carrying `tag` (stored without its dot) as the *original* error it stands for.
65 /// `__match` catches it and falls through; uncaught, it decays to `.tag`.
66 pub(crate) fn no_match(tag: &'static str) -> Raised {
67 Raised::NoMatch(Value::Symbol(Text::from_static(tag)))
68 }
69
70 /// Raise one of the host-failure symbol tags (stored without its leading dot) as
71 /// a genuine `Error`. The tag is a `'static` literal, so it becomes a
72 /// zero-cost borrowed [`Text`] via `from_static`.
73 pub(crate) fn symbol(tag: &'static str) -> Raised {
74 Raised::Error(Value::Symbol(Text::from_static(tag)))
75 }
76
77 /// Returns the carried payload regardless of the `Raised` kind.
78 pub fn value(&self) -> &Value {
79 match self {
80 Raised::Error(v) | Raised::NoMatch(v) => v,
81 }
82 }
83
84 /// Consumes the `Raised` value into its payload.
85 pub fn into_value(self) -> Value {
86 match self {
87 Raised::Error(v) | Raised::NoMatch(v) => v,
88 }
89 }
90}
91
92/// Evaluate an expression in the empty environment.
93///
94/// The [`Ctx`] threaded through evaluation carries the per-runtime **unique-id
95/// source**: each `&`-abstraction stamps its closure with the next id, giving
96/// callables an identity for equality/order.
97pub fn eval(expr: &Expr) -> Result<Value, Raised> {
98 let ctx = Ctx::new();
99 eval_env(expr, &Env::EMPTY, &ctx)
100}
101
102/// Apply a value to a single argument — the public entry point behind a host's
103/// "call this value" surface (e.g. the Python bindings' `elly.Fun.__call__`).
104///
105/// This is the same application step the evaluator uses internally (`apply_n`
106/// with one argument): a closure call, list projection, or one step toward
107/// saturating a builtin. Since values are `'static`-owned, `f` and `arg` may come
108/// from different programs. A fresh [`Ctx`] is used, mirroring [`eval`], so any
109/// closures minted while running the application are stamped from its id source.
110/// Use [`apply_with`] to apply under a context whose ids continue an earlier
111/// one's.
112pub fn apply(f: &Value, arg: Value) -> Result<Value, Raised> {
113 apply_with(f, arg, &Ctx::new())
114}
115
116/// Applies a value to a single argument using a caller-supplied [`Ctx`].
117/// This ensures identities minted by the call continue the provided context's
118/// ID source rather than restarting at zero. Required for hosts that manage
119/// callables across multiple `apply` calls to avoid ID collisions.
120pub fn apply_with(f: &Value, arg: Value, ctx: &Ctx) -> Result<Value, Raised> {
121 apply1(f.clone(), arg, ctx)
122}
123
124/// Evaluates an expression in a given environment and context.
125/// Public entry point for host evaluation (e.g., `elly.Env.eval`).
126pub fn eval_env(expr: &Expr, env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
127 match expr {
128 // Resolved reference: look up value by index.
129 Expr::Local { index, .. } => Ok(env.lookup(*index)),
130 // Resolver ensures all names are rewritten to `Expr::Local` before evaluation.
131 Expr::Name(n) => unreachable!("unresolved name {:?} reached eval", n),
132 // Module sibling reference (see [`mod_item`]).
133 Expr::ModItem { index, depth, .. } => mod_item(env, *index, *depth, ctx),
134 // Home-module leaf (see [`home_leaf`]).
135 Expr::Home { leaf, depth } => env.home_leaf(*leaf, *depth),
136 // Recursive binding group (see [`recur`]).
137 Expr::Recur(r) => recur(r, env, ctx),
138 // Block evaluation (see [`eval_block`]). Outlined to optimize I-cache.
139 Expr::Block(clauses) => eval_block(clauses, env, ctx),
140 // Builtin operation. 0-arity builtins execute immediately; others return
141 // a `Builtin` awaiting arguments.
142 Expr::Builtin(op) => {
143 if op.arity() == 0 {
144 builtins::invoke(*op, &[], ctx)
145 } else {
146 // Avoid `with_capacity` to prevent unnecessary allocations during
147 // curried application cloning.
148 Ok(Value::Builtin {
149 op: *op,
150 args: Vec::new(),
151 })
152 }
153 }
154 Expr::Int(n) => Ok(Value::from_bigint_ref(n)),
155 Expr::Symbol(s) => Ok(Value::Symbol(s.clone())),
156 // String handles are shared and pre-decoded.
157 Expr::Str(s) => Ok(Value::Str(s.clone())),
158 Expr::Unit => Ok(Value::Unit),
159 Expr::List(elems) => {
160 let mut vs = Vector::new();
161 for e in elems {
162 let elem = eval_env(e, env, ctx)?;
163 vs.push_back_mut(elem);
164 }
165 Ok(Value::List(vs))
166 }
167 // Map literal: evaluate keys and values in source order. Later entries
168 // with the same key overwrite previous ones.
169 Expr::Map(entries) => {
170 let mut m = HashTrieMap::new();
171 for (k, v) in entries {
172 let kv = eval_env(k, env, ctx)?;
173 let vv = eval_env(v, env, ctx)?;
174 m.insert_mut(kv, vv);
175 }
176 Ok(Value::Map(m))
177 }
178 // Closure creation: captures free variables according to the resolution plan.
179 // `Captures::Chain` shares the environment; `Captures::Frame` creates a new frame.
180 Expr::Abs(code) => {
181 let env = match &code.captures {
182 Captures::Chain => env.clone(),
183 Captures::Frame(plan) => env.capture_env(plan, ctx),
184 Captures::Unresolved => unreachable!("unresolved abstraction reached eval"),
185 };
186 Ok(Value::Closure {
187 id: ctx.next_id(),
188 code: Rc::clone(code),
189 applied: 0,
190 env,
191 })
192 }
193 // Call-by-value application. Callee and arguments are evaluated strictly
194 // left-to-right. Small arities (≤ 4) are handled on the stack to avoid
195 // heap allocation for the argument buffer.
196 Expr::App(items) => {
197 let callee = eval_env(&items[0], env, ctx)?;
198 let arg_exprs = &items[1..];
199 match arg_exprs {
200 [a] => {
201 let arg = eval_env(a, env, ctx)?;
202 apply1(callee, arg, ctx)
203 }
204 [a, b] => {
205 let va = eval_env(a, env, ctx)?;
206 let vb = eval_env(b, env, ctx)?;
207 apply_n(callee, &[va, vb], ctx)
208 }
209 [a, b, c] => {
210 let va = eval_env(a, env, ctx)?;
211 let vb = eval_env(b, env, ctx)?;
212 let vc = eval_env(c, env, ctx)?;
213 apply_n(callee, &[va, vb, vc], ctx)
214 }
215 [a, b, c, d] => {
216 let va = eval_env(a, env, ctx)?;
217 let vb = eval_env(b, env, ctx)?;
218 let vc = eval_env(c, env, ctx)?;
219 let vd = eval_env(d, env, ctx)?;
220 apply_n(callee, &[va, vb, vc, vd], ctx)
221 }
222 _ => {
223 let mut argv: Vec<Value> = Vec::with_capacity(arg_exprs.len());
224 for a in arg_exprs {
225 argv.push(eval_env(a, env, ctx)?);
226 }
227 apply_n(callee, &argv, ctx)
228 }
229 }
230 }
231 // Subject is evaluated once, then arms are tried in order.
232 Expr::Case { subject, cases } => {
233 let subj = eval_env(subject, env, ctx)?;
234 run_arms(&subj, cases, env, ctx)
235 }
236 }
237}
238
239/// Tries case arms in order. `NoMatch` refutations fall through; other errors propagate.
240/// If no arms match, `.no_match` is raised.
241#[inline(never)]
242fn run_arms(
243 subj: &Value,
244 cases: &[(Pattern, Expr)],
245 env: &Env,
246 ctx: &Ctx,
247) -> Result<Value, Raised> {
248 for (pat, body) in cases {
249 match match_pattern(pat, subj, env.clone(), ctx) {
250 // Not released explicitly: only 0.05% of node deaths happen here, and
251 // holding the result across a release grows this recursive frame.
252 Ok(arm_env) => return eval_env(body, &arm_env, ctx),
253 Err(Raised::NoMatch(_)) => continue,
254 Err(e) => return Err(e),
255 }
256 }
257 Err(Raised::no_match("no_match"))
258}
259
260/// Specialized path for single-argument application. Used by the call spine,
261/// fold builtins, and match clauses. Binds one parameter, produces a partial
262/// closure, gathers a builtin argument, or projects a list.
263pub(crate) fn apply1(fv: Value, av: Value, ctx: &Ctx) -> Result<Value, Raised> {
264 match fv {
265 Value::Closure {
266 code, applied, env, ..
267 } => {
268 let start = applied as usize;
269 // Bind argument and extend environment.
270 let call_env = match_pattern(&code.head[start], &av, env, ctx)?;
271 if start + 1 < code.head.len() {
272 // Return partial closure with updated applied count and environment.
273 return Ok(Value::Closure {
274 id: ctx.next_id(),
275 code,
276 applied: applied + 1,
277 env: call_env,
278 });
279 }
280 let result = eval_env(&code.body, &call_env, ctx);
281 ctx.release(call_env);
282 result
283 }
284 Value::Builtin {
285 op,
286 args: mut gathered,
287 } => {
288 // Unary builtins saturate from the stack.
289 if gathered.is_empty() && op.arity() == 1 {
290 return builtins::invoke(op, &[av], ctx);
291 }
292 gathered.push(av);
293 if gathered.len() == op.arity() {
294 builtins::invoke(op, &gathered, ctx)
295 } else {
296 Ok(Value::Builtin { op, args: gathered })
297 }
298 }
299 Value::HostFn {
300 code,
301 args: gathered,
302 } => {
303 if gathered.is_empty() && code.arity == 1 {
304 return (code.f)(&[av], ctx);
305 }
306 let gathered = extended(&gathered, &[av]);
307 if gathered.len() == code.arity {
308 (code.f)(&gathered, ctx)
309 } else {
310 Ok(Value::HostFn {
311 code,
312 args: gathered,
313 })
314 }
315 }
316 // Materialize item when a module is applied to a symbol.
317 Value::Module(home) => match av {
318 Value::Symbol(s) => materialize(&Env::from_ref(home), &s, ctx),
319 _ => Err(Raised::symbol("not_applicable")),
320 },
321 // Dispatch via prototype for all other types.
322 fv => receiver(fv, &av, &[], ctx),
323 }
324}
325
326/// Applies a value to multiple arguments. Handles closures (multi-parameter
327/// matching), builtins (gather until saturation), and list projections.
328/// Over-application results loop onto the returned value.
329///
330/// Arguments are evaluated strictly left-to-right before matching patterns
331/// left-to-right, ensuring evaluation errors surface before pattern refutations.
332/// See `docs/done/2026-08-02_perf-less-currying.md` §3.
333fn apply_n(mut fv: Value, mut args: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
334 while !args.is_empty() {
335 match fv {
336 // Matches arguments against remaining patterns left-to-right.
337 // If arguments are exhausted, returns a partial closure with a fresh ID.
338 // Otherwise, executes the body and loops leftover arguments onto the result.
339 // Pattern misses raise `NoMatch`.
340 Value::Closure {
341 code, applied, env, ..
342 } => {
343 let start = applied as usize;
344 let remaining = code.head.len() - start;
345 let take = remaining.min(args.len());
346 // Thread the env through each parameter match, consing bindings
347 // onto the closure's captured (or partially-applied) env.
348 let mut call_env = env;
349 for (pat, arg) in code.head[start..start + take].iter().zip(&args[..take]) {
350 call_env = match_pattern(pat, arg, call_env, ctx)?;
351 }
352 if take < remaining {
353 // Partial: no body runs yet; mint a fresh identity.
354 return Ok(Value::Closure {
355 id: ctx.next_id(),
356 code,
357 applied: applied + take as u8,
358 env: call_env,
359 });
360 }
361 let result = eval_env(&code.body, &call_env, ctx);
362 ctx.release(call_env);
363 fv = result?;
364 args = &args[take..];
365 }
366 // Gathers arguments and invokes once saturated, looping leftovers onto the result.
367 // If the argument spine satisfies the arity immediately, it invokes directly
368 // from the slice to avoid `Value::Builtin` allocation.
369 Value::Builtin {
370 op,
371 args: mut gathered,
372 } => {
373 let arity = op.arity();
374 if gathered.is_empty() && args.len() >= arity {
375 fv = builtins::invoke(op, &args[..arity], ctx)?;
376 args = &args[arity..];
377 } else {
378 let take = (arity - gathered.len()).min(args.len());
379 gathered.extend(args[..take].iter().cloned());
380 if gathered.len() < arity {
381 return Ok(Value::Builtin { op, args: gathered });
382 }
383 fv = builtins::invoke(op, &gathered, ctx)?;
384 args = &args[take..];
385 }
386 }
387 // Similar gather-and-fire loop as builtins, dispatching via the host callback.
388 Value::HostFn {
389 code,
390 args: gathered,
391 } => {
392 let arity = code.arity;
393 if gathered.is_empty() && args.len() >= arity {
394 fv = (code.f)(&args[..arity], ctx)?;
395 args = &args[arity..];
396 } else {
397 let take = (arity - gathered.len()).min(args.len());
398 let gathered = extended(&gathered, &args[..take]);
399 if gathered.len() < arity {
400 return Ok(Value::HostFn {
401 code,
402 args: gathered,
403 });
404 }
405 fv = (code.f)(&gathered, ctx)?;
406 args = &args[take..];
407 }
408 }
409 // Materializes an item when applied to a symbol; leftovers loop onto the result.
410 Value::Module(home) => {
411 match &args[0] {
412 Value::Symbol(s) => fv = materialize(&Env::from_ref(home), s, ctx)?,
413 _ => return Err(Raised::symbol("not_applicable")),
414 }
415 args = &args[1..];
416 }
417 // Dispatches by passing the entire remaining argument spine to the
418 // method in one call (see [`receiver`]).
419 fv => return receiver(fv, &args[0], &args[1..], ctx),
420 }
421 }
422 Ok(fv)
423}
424
425/// Concatenates `prefix` and `more` into a single allocation.
426/// Used by [`Value::HostFn`] for efficient argument gathering during partial application.
427fn extended(prefix: &[Value], more: &[Value]) -> Box<[Value]> {
428 let mut all = Vec::with_capacity(prefix.len() + more.len());
429 all.extend_from_slice(prefix);
430 all.extend_from_slice(more);
431 all.into_boxed_slice()
432}
433
434/// Returns the prototype of a value. Objects use their own prototype; others
435/// use the corresponding builtin module, providing a nominal type for all values.
436///
437/// Iotas resolve to their declaring module, allowing them to be treated as
438/// objects of that module. Callables (closures, builtins, host functions)
439/// resolve to `__Fun`.
440pub(crate) fn proto_of(v: &Value, ctx: &Ctx) -> Value {
441 match v {
442 Value::Object(obj) => obj.proto(),
443 Value::Iota { home, .. } => Value::Module(home.clone()),
444 Value::I64(_) | Value::BigInt(_) => ctx.builtin_module(Builtin::Int),
445 Value::Symbol(_) => ctx.builtin_module(Builtin::Sym),
446 Value::Str(_) => ctx.builtin_module(Builtin::Str),
447 Value::Unit => ctx.builtin_module(Builtin::Unit),
448 Value::List(_) => ctx.builtin_module(Builtin::List),
449 Value::Map(_) => ctx.builtin_module(Builtin::Map),
450 Value::Closure { .. } | Value::Builtin { .. } | Value::HostFn { .. } => {
451 ctx.builtin_module(Builtin::Fun)
452 }
453 Value::Module(_) => ctx.builtin_module(Builtin::Mod),
454 }
455}
456
457/// Whether `value`'s prototype is the module `expr` names — the general form of an
458/// `as <Proto>` pattern, where [`ProtoRef::Kind`] is the compiled one. A reference
459/// that is not a module raises `.not_a_module`: it is a mistake about the pattern
460/// rather than about the subject, so it is a real error and not a refutation.
461///
462/// Outlined for the reason [`receiver`] is: `match_pattern` is on every call's
463/// path, and its arms pay for each other's code size.
464#[inline(never)]
465pub(crate) fn proto_is(expr: &Expr, value: &Value, env: &Env, ctx: &Ctx) -> Result<bool, Raised> {
466 let expected = eval_env(expr, env, ctx)?;
467 if !matches!(expected, Value::Module(_)) {
468 return Err(Raised::symbol("not_a_module"));
469 }
470 Ok(proto_of(value, ctx) == expected)
471}
472
473/// Resolves a module sibling reference by walking to the home module and
474/// materializing the item at `index`.
475///
476/// The index is assigned during resolution against the module's sorted table,
477/// enabling direct slice access. Only module-item bodies contain `ModItem`,
478/// and they always run under a `Module` home terminal, which is found by
479/// walking through captured frames.
480///
481/// Outlined to maintain I-cache residency of `eval_env`.
482///
483/// If the index is past the item table, it is treated as an iota, which is
484/// derived as a pair of the terminal and position.
485///
486/// `depth` specifies how many terminals to skip to find the owning module.
487/// A `recur` group nested in a module body adds a terminal to the chain, requiring
488/// a skip for the enclosing module's items.
489#[inline(never)]
490fn mod_item(env: &Env, index: u32, depth: u32, ctx: &Ctx) -> Result<Value, Raised> {
491 let (env, home, data, id_base) = env
492 .find_module(depth)
493 .ok_or_else(|| Raised::symbol("no_module"))?;
494 let Some(body) = data.item(index) else {
495 // Handle iotas: derived from terminal identity and index.
496 let index = index - data.len() as u32;
497 if data.iota(index).is_none() {
498 return Err(Raised::symbol("missing_property"));
499 }
500 return Ok(Value::Iota {
501 home: home.clone(),
502 index,
503 });
504 };
505 materialize_body(body, env, id_base, index, ctx)
506}
507
508/// Evaluates a `recur` group by creating a `Module` terminal over the
509/// current environment and executing the body.
510///
511/// This is instantiation with a cons-shaped frame, avoiding copies by
512/// sharing the lexical environment. ID blocks are reserved per evaluation,
513/// ensuring different activations of the same `recur` group have distinct
514/// item identities. Bindings are materialized on reference.
515#[inline(never)]
516fn recur(group: &Rc<ModuleData>, env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
517 let home = EnvRef::new(EnvNode::Module {
518 id_base: ctx.reserve_ids(group.len() as u64),
519 data: Rc::clone(group),
520 frame: env.clone(),
521 });
522 eval_env(recur_body(group), &Env::from_ref(home), ctx)
523}
524
525/// Evaluates a block by threading the environment through clauses.
526/// Bindings extend the environment for subsequent clauses; other clauses
527/// execute for effect. The value of the final clause is returned.
528fn eval_block(clauses: &[(Option<Pattern>, Expr)], env: &Env, ctx: &Ctx) -> Result<Value, Raised> {
529 let Some((last, leading)) = clauses.split_last() else {
530 return Ok(Value::Unit);
531 };
532 let mut env = env.clone();
533 for clause in leading {
534 match clause {
535 (Some(pat), value) => {
536 let v = eval_env(value, &env, ctx)?;
537 env = match_pattern(pat, &v, env, ctx)?;
538 }
539 (None, value) => {
540 eval_env(value, &env, ctx)?;
541 }
542 }
543 }
544 match last {
545 (None, value) => {
546 let result = eval_env(value, &env, ctx);
547 ctx.release(env);
548 result
549 }
550 (Some(pat), value) => {
551 let v = eval_env(value, &env, ctx)?;
552 ctx.release(match_pattern(pat, &v, env, ctx)?);
553 Ok(v)
554 }
555 }
556}
557
558/// Instantiates a compiled module, creating its [`Module`](EnvNode::Module)
559/// terminal and returning it as a [`Value::Module`].
560///
561/// This assigns the module an identity (fresh `Rc`) and reserves its ID block
562/// so items can be stamped with stable IDs (see `materialize_body`).
563///
564/// `frame` provides outer values against [`ModuleData::frame_names`]. A length
565/// mismatch raises `.module_arity`. The frame is stored as a flat [`Frame`](EnvNode::Frame),
566/// though `recur` may build the terminal directly.
567pub fn instantiate(data: Rc<ModuleData>, frame: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
568 if frame.len() != data.frame_names().len() {
569 return Err(Raised::symbol("module_arity"));
570 }
571 let id_base = ctx.reserve_ids(data.len() as u64);
572 let frame = if frame.is_empty() {
573 Env::EMPTY
574 } else {
575 Env::EMPTY.frame(Box::from(frame))
576 };
577 Ok(Value::Module(EnvRef::new(EnvNode::Module {
578 data,
579 id_base,
580 frame,
581 })))
582}
583
584/// Evaluates an item's body in its module terminal. This is the common
585/// path for [`mod_item`] and [`materialize`].
586///
587/// Literal abstraction bodies are built directly with the item's reserved
588/// ID (`id_base + index`), ensuring multiple accesses produce equal closures.
589/// This avoids `eval_env` dispatch and `Ctx` counter writes for performance.
590///
591/// Only function definitions claim the item's identity. Forwarded callables
592/// retain their original identity, while data items or nested closures are
593/// processed via `eval_env`.
594#[inline]
595fn materialize_body(
596 body: &Expr,
597 env: &Env,
598 id_base: u64,
599 index: u32,
600 ctx: &Ctx,
601) -> Result<Value, Raised> {
602 if let Expr::Abs(code) = body {
603 let env1 = match &code.captures {
604 Captures::Chain => env.clone(),
605 Captures::Frame(plan) => env.capture_env(plan, ctx),
606 Captures::Unresolved => unreachable!("unresolved abstraction reached eval"),
607 };
608 return Ok(Value::Closure {
609 id: id_base + index as u64,
610 code: Rc::clone(code),
611 applied: 0,
612 env: env1,
613 });
614 }
615 eval_env(body, env, ctx)
616}
617
618/// Materializes the item named `name` from the module object `home`.
619/// Binary-searches for the index and evaluates the body in the terminal.
620/// Uses the object's own node as the terminal to avoid environment allocation.
621/// Raises `.missing_property` if the name is absent.
622fn materialize(env: &Env, name: &Text, ctx: &Ctx) -> Result<Value, Raised> {
623 let (moddata, id_base) = env
624 .as_module()
625 .expect("a module object always holds a module terminal");
626 let index = moddata
627 .index_of(name)
628 .ok_or_else(|| Raised::symbol("missing_property"))?;
629 let body = moddata.item(index).expect("index_of returned a live index");
630 materialize_body(body, env, id_base, index, ctx)
631}
632
633/// Applies a receiver to a symbol. This is the dispatch path for all non-callable,
634/// non-module values. The symbol identifies a member of the receiver's prototype,
635/// and the receiver is passed as an argument. [`proto_of`] ensures this is total.
636///
637/// List projection takes precedence: numeric symbols on lists project indices.
638/// Other symbols dispatch to `__List`.
639///
640/// Non-symbol arguments raise `.not_applicable`.
641///
642/// Outlined to maintain I-cache residency in `apply1` and `apply_n`.
643#[inline(never)]
644fn receiver(this: Value, name: &Value, rest: &[Value], ctx: &Ctx) -> Result<Value, Raised> {
645 let Value::Symbol(s) = name else {
646 return Err(Raised::symbol("not_applicable"));
647 };
648 if let Value::List(elems) = &this {
649 if is_index(s) {
650 let projected = project(elems, s)?;
651 return apply_n(projected, rest, ctx);
652 }
653 }
654 let proto = if let Value::Object(obj) = &this {
655 obj.proto.clone()
656 } else if let Value::Module(home) = proto_of(&this, ctx) {
657 home
658 } else {
659 unreachable!("a prototype is always a module value")
660 };
661 dispatch(proto, this, s, rest, ctx)
662}
663
664/// Dispatches a method by materializing `name` from `proto` and applying it
665/// to the receiver and any remaining arguments.
666///
667/// Example: `x .m a b` is equivalent to `(materialize (proto of x) .m) x a b`.
668///
669/// The lookup is a binary search over the item table, ensuring locals are private.
670/// Arguments are handled similarly to the call spine in `eval_env`, with small
671/// arities processed on the stack.
672fn dispatch(
673 proto: EnvRef,
674 this: Value,
675 name: &Text,
676 rest: &[Value],
677 ctx: &Ctx,
678) -> Result<Value, Raised> {
679 let method = materialize(&Env::from_ref(proto), name, ctx)?;
680 match rest {
681 [] => apply1(method, this, ctx),
682 [a] => apply_n(method, &[this, a.clone()], ctx),
683 [a, b] => apply_n(method, &[this, a.clone(), b.clone()], ctx),
684 [a, b, c] => apply_n(method, &[this, a.clone(), b.clone(), c.clone()], ctx),
685 _ => {
686 let mut all: Vec<Value> = Vec::with_capacity(rest.len() + 1);
687 all.push(this);
688 all.extend_from_slice(rest);
689 apply_n(method, &all, ctx)
690 }
691 }
692}
693
694/// Whether `sym` (a symbol's text, stored without its dot) is a **projection**
695/// index — all ASCII digits, so `.0` and `.12` are projections while `.get` and
696/// `.size` are member names. The two spaces are disjoint by shape, which is what
697/// lets a list carry both (see [`receiver`]).
698fn is_index(sym: &str) -> bool {
699 !sym.is_empty() && sym.bytes().all(|b| b.is_ascii_digit())
700}
701
702/// Projects the element at index `sym` from a list.
703fn project(elems: &Vector<Value>, sym: &str) -> Result<Value, Raised> {
704 let idx: usize = sym.parse().map_err(|_| Raised::symbol("bad_projection"))?;
705 elems
706 .get(idx)
707 .cloned()
708 .ok_or_else(|| Raised::symbol("projection_out_of_range"))
709}