elly_core/resolve.rs
1//! Name resolution: a static pass over the parsed AST that turns each in-scope
2//! [`Expr::Name`] into a de Bruijn-indexed [`Expr::Local`] and rejects free
3//! variables before evaluation.
4//!
5//! Every binding in Elly comes from one place — a [`crate::ast::Lambda`]
6//! head matched against arguments — and there is no global/prelude environment
7//! (builtins are hoisted to [`Expr::Builtin`] at parse). So every `Expr::Name`
8//! either resolves to an enclosing `&`-binding or is **unbound**, and that is
9//! decidable statically. [`resolve`] rewrites the tree in place and returns
10//! [`ParseError::UnboundName`] on the first free name; wiring it into
11//! `elly::parse` turns an unbound reference into a parse-time error instead of a
12//! runtime `.unbound_name` raise.
13//!
14//! ## de Bruijn indices, per activation
15//!
16//! The runtime environment of a lambda activation is a nameless cons list of the
17//! bindings that activation's head made — innermost/most-recent first — sitting on
18//! the closure's captured **frame** (see `EnvNode` in `eval.rs`). The resolver
19//! mirrors that shape with a stack of [`Level`]s, one per enclosing
20//! `&`-abstraction, each holding the names its head binds (`locals`, in bind order)
21//! and the free variables it captures (`captures`, in frame-slot order).
22//!
23//! A reference is resolved against the *innermost* level:
24//!
25//! 1. bound by this level's head → `locals.len() - 1 - pos` of the binder's
26//! (rightmost, so most recent) occurrence: the number of cells the matcher will
27//! have consed between the reference and its binder. A duplicate bind (`[a, a]`)
28//! pushes twice; the later one shadows, reproducing the old last-one-wins;
29//! 2. already captured by this level → `locals.len() + slot`: the walk runs off the
30//! consed cells and lands in the frame at `slot`;
31//! 3. otherwise → resolve it in the **parent** level (recursively, so a name may be
32//! captured down through several lambdas), record that outer index as a new
33//! capture slot of this level, and use `locals.len() + slot` as in (2).
34//!
35//! Step 3 is what fills [`Lambda::captures`](crate::ast::Lambda::captures): the
36//! outer indices, in the enclosing activation's coordinates *at the point the `Abs`
37//! is evaluated*, of the values to copy into the closure's frame. Those coordinates
38//! are stable while the inner lambda is walked — the parent's `locals` do not change
39//! during it — and a capture slot, once assigned, is never renumbered.
40//!
41//! At the outermost level there is no parent, so a name unknown there is unbound
42//! ([`resolve`]), auto-bound into the root frame ([`resolve_open`]), or found in the
43//! prelude ([`resolve_prelude`]).
44//!
45//! The walk mirrors the evaluator's *binding order* exactly (see `match_pattern`
46//! in `eval.rs`): a list pattern binds its rest before its elements, a map pattern
47//! evaluates all lookup keys before binding any value pattern, and an `= expr` /
48//! `< expr` / `> expr` comparand (and a map lookup key) resolves against the names
49//! bound *before* it — including earlier binders in the same pattern, e.g.
50//! `&[a, = a]`. Getting this order right is what makes "accepted here" ⟺ "not an
51//! unbound raise there", and puts each reference's index where the matcher will
52//! have consed the value.
53//!
54//! ## Or-arms
55//!
56//! Both arms of `(p | q)` must bind the **same names in the same order**: under a
57//! nameless cons list a reference after the or-pattern reads one fixed index
58//! whichever arm matched, so the two arms must cons an identical binding sequence.
59//! The resolver has that order naturally — it walks each arm and compares the
60//! names they push — and rejects a mismatch with [`ParseError::OrBindersMismatch`]
61//! (this tightens the old same-*set* rule to same-*sequence*; reordered arms like
62//! `[x, y] | [y, x]` are now an error).
63//!
64//! An arm may also *capture* — `&([a, = x] | [a, = y])` reads two outer names — and
65//! a capture is not undone when the left arm's locals are rolled back: a frame slot
66//! is assigned once and holds whichever value the enclosing scope had, regardless of
67//! which arm runs. Only the arms' `locals` take part in the sequence check.
68
69use alloc::boxed::Box;
70use alloc::rc::Rc;
71use alloc::vec::Vec;
72
73use crate::ast::{Capture, Captures, Expr, Lambda, MapKey, Pattern, ProtoRef, Rest, Text};
74use crate::eval::{Builtin, ModuleData};
75use crate::parse::ParseError;
76
77/// Rebuild a call spine `[<module>, .member, rest…]` around the member's own
78/// builtin leaf: `__Int.add x y` becomes `__Int.add` applied to `x y`, and a bare
79/// `__Int.add` becomes the leaf itself.
80///
81/// The remaining arguments are **moved** out of the old spine — each swapped for
82/// the copyable leaf rather than cloned — so folding a call whose arguments are
83/// large subtrees copies no tree.
84fn fold_member(items: &mut [Expr], op: Builtin) -> Expr {
85 let rest = &mut items[2..];
86 if rest.is_empty() {
87 return Expr::Builtin(op);
88 }
89 let mut spine: Vec<Expr> = Vec::with_capacity(rest.len() + 1);
90 spine.push(Expr::Builtin(op));
91 for arg in rest {
92 spine.push(core::mem::replace(arg, Expr::Builtin(op)));
93 }
94 Expr::App(spine.into_boxed_slice())
95}
96
97/// Resolve a whole program against an empty scope: every free name is rejected
98/// with [`ParseError::UnboundName`]. This is what `elly::parse` runs, so an
99/// unbound reference is a parse-time error. Rewrites `expr` in place (names →
100/// de Bruijn locals).
101pub fn resolve(expr: &mut Expr) -> Result<(), ParseError> {
102 Resolver::new(Vec::new(), false, Vec::new()).walk(expr)
103}
104
105/// Resolve each body of a module's bindings in place (name → de Bruijn `Local`,
106/// sibling item → [`Expr::ModItem`], else [`ParseError::UnboundName`]). The item
107/// names form an extra reference tier consulted **after** lexical scope and before
108/// the unbound check, so a body may refer to any sibling regardless of source
109/// order — the basis of order-independent, mutually recursive top-level
110/// definitions (see `docs/done/2026-08-07_elly-modules-v0.md`). Called by
111/// [`crate::compile_module`] after [`crate::parse::parse_module`].
112///
113/// The bindings are first **sorted by name**: that order is the module's item
114/// table, and a sibling reference records its position there as the `ModItem`
115/// index, so neither this pass nor eval compares names down a list. `parse_module`
116/// rejects duplicates, so the names are distinct and the order is total.
117///
118/// A name in a module body is looked for in this order: the enclosing `&`/`let`
119/// binders, then the module's `imports`, then its own members — `iotas` before
120/// items and locals — then the rest of the `frame`, then the builtin module
121/// aliases. Anything found in the frame — imports and the outer values alike —
122/// becomes an ordinary `Local` whose index walks *past* the module terminal into
123/// the frame, so it costs one indexed lookup and evaluates nothing.
124///
125/// `locals` are the module's `local` declarations: members exactly as the items
126/// are — so a body reaches one the same way, and a local's own body is resolved
127/// here alongside the items' — differing only in being addressed *after* the
128/// items, which is where an outside `M.name` access stops looking. They are
129/// sorted here as the bindings are, and the two orders concatenated are the
130/// module's id-bearing address space.
131///
132/// `main` is the module's `__main`, when a source declared one: the **reserved
133/// moditem** a runner evaluates. It is resolved here, with the item bodies and
134/// against the same tiers, because it is written in the module's own scope. It
135/// is not a member, so it claims no index and shifts nothing — the member
136/// address space is exactly what it was before a source grew one.
137///
138/// `imports` names the module's own imports and `frame` whatever the compile was
139/// given (a host prelude); together, imports first, they are the frame the module
140/// is instantiated against. `iotas` names its `const` declarations, which take no
141/// slot: they are members addressed *past* the items and the locals, so an iota's
142/// `ModItem` index is `items.len() + locals.len() + its position`. Pass empty
143/// slices for a module with none of them.
144///
145/// **Iotas are looked for before items** for the reason imports are: the
146/// re-exporting form `false = const` is one declaration yielding both an iota and
147/// an item forwarding it, and a sibling reference wants the iota rather than the
148/// item that would evaluate to it.
149///
150/// **Imports are looked for before items** because of the re-exporting form
151/// `Foo = import "spec"`, which is one declaration that yields both a frame slot
152/// and an item forwarding it. Searching items first would send every body's `Foo`
153/// through the forwarding item, evaluating it to reach the slot the body wanted;
154/// finding the slot directly means each reference reads the one instance and the
155/// item is left for `M.Foo` from outside. No other name can be both, since
156/// `parse_module` rejects an import and an item that share one.
157pub fn resolve_module_bodies(
158 bindings: &mut [(Text, Expr)],
159 locals: &mut [(Text, Expr)],
160 main: Option<&mut Expr>,
161 imports: &[Text],
162 iotas: &[Text],
163 frame: &[Text],
164) -> Result<(), ParseError> {
165 bindings.sort_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
166 locals.sort_by(|(a, _), (b, _)| a.as_str().cmp(b.as_str()));
167 let members = Barrier::Module {
168 items: bindings.iter().map(|(n, _)| n.clone()).collect(),
169 locals: locals.iter().map(|(n, _)| n.clone()).collect(),
170 iotas: iotas.to_vec(),
171 };
172 let mut slots: Vec<Text> = imports.to_vec();
173 slots.extend_from_slice(frame);
174 let bodies = bindings
175 .iter_mut()
176 .chain(locals.iter_mut())
177 .map(|(_, body)| body)
178 // The module's own body — `__main`, when a source declared one — is
179 // resolved here rather than by a later pass because it is written in the
180 // same scope the item bodies are, and reaches the same three tiers.
181 .chain(main);
182 for body in bodies {
183 Resolver::new(slots.clone(), false, alloc::vec![members.clone()])
184 .with_imports(imports.len())
185 .walk(body)?;
186 }
187 Ok(())
188}
189
190/// Resolve an **open** term: a free name is not rejected but auto-bound below the
191/// whole scope, so the pass runs to completion on a fragment. Used by tooling and
192/// by the parse golden suite, which resolves open terms (their s-expressions read
193/// like the spec) without a rejection — the real index assignment and or-arm
194/// agreement still run, and names render by name, so the goldens do not change.
195/// Prefer [`resolve`] for a whole program.
196pub fn resolve_open(expr: &mut Expr) -> Result<(), ParseError> {
197 Resolver::new(Vec::new(), true, Vec::new()).walk(expr)
198}
199
200/// Resolve `expr` against a named outer `prelude` (insertion order): a reference
201/// to `prelude[i]` becomes the index `i` of the root frame — reached directly at
202/// the top level, or through a lambda's capture list from inside one — and any name
203/// neither in scope nor in `prelude` is `UnboundName`. Like `resolve_open` but the
204/// root frame is fixed and complete (no auto-bind). Used by `elly::parse_preluded`,
205/// whose caller supplies the prelude values in that same order.
206pub fn resolve_prelude(expr: &mut Expr, prelude: &[Text]) -> Result<(), ParseError> {
207 Resolver::new(prelude.to_vec(), false, Vec::new()).walk(expr)
208}
209
210/// One lambda activation's compile-time scope: the names its head binds and the
211/// frame it captures. The resolver keeps a stack of these, one per enclosing
212/// `&`-abstraction, plus a root level standing for the environment the whole term
213/// is evaluated in (see the module docs).
214struct Level {
215 /// The names this activation's head binds, in cons order (most recent last),
216 /// one entry per cell the matcher conses. Usually empty for the root level,
217 /// but not always: a [`Captures::Chain`] abstraction opens no level, so a
218 /// top-level `(&x …) v` binds `x` into the root.
219 locals: Vec<Text>,
220 /// The free names this lambda captures, in frame-slot order. For the root
221 /// level these are the prelude / auto-bound names instead.
222 captures: Vec<Text>,
223 /// Parallel to `captures`: each capture's index in the **enclosing**
224 /// activation's coordinates, i.e. what the closure looks up when it is built.
225 /// This is the list stored into [`Lambda::captures`](crate::ast::Lambda) when
226 /// the level pops. Unused (and empty) for the root level, which is nobody's
227 /// closure.
228 outer: Vec<u32>,
229}
230
231impl Level {
232 fn new() -> Level {
233 Level {
234 locals: Vec::new(),
235 captures: Vec::new(),
236 outer: Vec::new(),
237 }
238 }
239}
240
241/// One terminal's **member set**: the names reachable through it, which is what a
242/// [`Expr::ModItem`] addresses. A module's parts share one index space — items,
243/// then locals, then iotas — so `index` is a member's position in that space,
244/// `items.len()` is where the private bindings begin and
245/// `items.len() + locals.len()` where the iotas do.
246///
247/// Each part is name-sorted, so a lookup is three binary searches at most. The
248/// order they are searched in is free: the names are disjoint, since a module
249/// declares each of them once. Iotas go first for the reason
250/// [`resolve_module_bodies`] gives.
251///
252/// **The two kinds are distinguished here** because a terminal reached by walking
253/// out is not always the one a construct means: the home-module leaves —
254/// `__module`, `__new`, `__value` — name the nearest terminal *written as a
255/// module*, stepping over any group in between, so the stack has to say which of
256/// its entries is which. The kind is known at both construction sites and nowhere
257/// derivable afterwards: a group has no canonical name, but neither need a module
258/// (a host may compile one unnamed), and a group's locals and iotas are empty, but
259/// so are an empty module's. See `docs/done/2026-08-15_elly-objects-v0.md`.
260#[derive(Clone)]
261enum Barrier {
262 /// A [`recur`](Expr::Recur) group's bindings. They are members exactly as a
263 /// module's items are, and a group may declare neither a local
264 /// ([`ParseError::MisplacedLocal`]) nor an iota, so the index space is the
265 /// item table alone.
266 Group { items: Vec<Text> },
267 /// A module's declarations, in the one index space described above.
268 Module {
269 items: Vec<Text>,
270 locals: Vec<Text>,
271 iotas: Vec<Text>,
272 },
273}
274
275impl Barrier {
276 /// `name`'s position in the member index space, or `None`.
277 fn member(&self, name: &str) -> Option<u32> {
278 let (items, locals, iotas) = match self {
279 Barrier::Group { items } => return search(items, name),
280 Barrier::Module {
281 items,
282 locals,
283 iotas,
284 } => (items, locals, iotas),
285 };
286 if let Some(i) = search(iotas, name) {
287 return Some(i + (items.len() + locals.len()) as u32);
288 }
289 if let Some(i) = search(items, name) {
290 return Some(i);
291 }
292 search(locals, name).map(|i| i + items.len() as u32)
293 }
294
295 /// The error a binder taking `name` earns, or `None` if the name is not
296 /// declared here — the answer [`check_shadow`](Resolver::check_shadow) reports.
297 /// Which of the three it is is the whole reason this is not `member`.
298 fn shadow_error(&self, name: &Text) -> Option<ParseError> {
299 match self {
300 Barrier::Group { items } => {
301 search(items, name.as_str()).map(|_| ParseError::ShadowsItem(name.clone()))
302 }
303 Barrier::Module {
304 items,
305 locals,
306 iotas,
307 } => {
308 if search(iotas, name.as_str()).is_some() {
309 return Some(ParseError::ShadowsIota(name.clone()));
310 }
311 if search(items, name.as_str()).is_some() {
312 return Some(ParseError::ShadowsItem(name.clone()));
313 }
314 search(locals, name.as_str()).map(|_| ParseError::ShadowsLocal(name.clone()))
315 }
316 }
317 }
318}
319
320/// `name`'s position in one name-sorted member part, or `None`.
321fn search(part: &[Text], name: &str) -> Option<u32> {
322 part.binary_search_by(|it| it.as_str().cmp(name))
323 .ok()
324 .map(|i| i as u32)
325}
326
327/// A lexical resolver: a stack of [`Level`]s, innermost last, with `levels[0]` the
328/// root. `auto_bind` selects the behaviour on a name unknown at the root — reject
329/// it ([`resolve`]) or absorb it into the root frame ([`resolve_open`]).
330struct Resolver {
331 levels: Vec<Level>,
332 auto_bind: bool,
333 /// The enclosing **member sets**, outermost first — one per module terminal on
334 /// the environment chain: a file module's items and iotas, then one per
335 /// enclosing [`recur`](Expr::Recur), each saying which of the two it is.
336 /// Together they are the reference tier
337 /// between the lexical scope and the root frame, scanned innermost-first so a
338 /// nested `recur`'s bindings shadow an outer group's, and the stack distance to
339 /// the set that answers is the [`Expr::ModItem`] `depth`. Empty for an ordinary
340 /// program outside any module — where a `recur` still pushes a set, so a stack
341 /// one deep is not a module by itself.
342 ///
343 /// The names are cloned in rather than borrowed: the sets live inside the very
344 /// tree being rewritten, and a `recur`'s bodies are walked mutably while its
345 /// own set is on the stack. A group is small and resolution runs once.
346 barriers: Vec<Barrier>,
347 /// How many of the root frame's leading slots are the module's own **imports**
348 /// — the slots looked for *before* the item sets rather than after them (see
349 /// [`resolve_module_bodies`]). Zero everywhere else, which puts the whole root
350 /// frame after the items as before.
351 imports: usize,
352}
353
354/// What a name resolved to: a de Bruijn index into the environment, or a sibling
355/// item of the enclosing module. The two are different *kinds* of reference, not
356/// two indices — an item is not a value in the environment and costs no capture
357/// slot — so [`reference`](Resolver::reference) reports which it found rather than
358/// returning an index that the caller would have to interpret.
359///
360/// [`reference`]: Resolver::reference
361enum Ref {
362 Local(u32),
363 /// An item of an enclosing group: its position in that group's sorted table,
364 /// and how many module terminals lie between the reference and the group.
365 Item {
366 index: u32,
367 depth: u32,
368 },
369 /// A **builtin module** reached through its bare alias (`Int` → `__Int`): the
370 /// resolver's bottom tier, below everything a name could be bound to. Like an
371 /// `Item` it is not a value in the environment, so it costs no capture slot; it
372 /// is a leaf, so it costs nothing at all.
373 Builtin(Builtin),
374}
375
376impl Resolver {
377 /// A resolver whose root frame holds `root` (a prelude, or empty).
378 fn new(root: Vec<Text>, auto_bind: bool, barriers: Vec<Barrier>) -> Resolver {
379 Resolver {
380 levels: alloc::vec![Level {
381 locals: Vec::new(),
382 captures: root,
383 outer: Vec::new(),
384 }],
385 auto_bind,
386 barriers,
387 imports: 0,
388 }
389 }
390
391 /// Mark the first `n` root frame slots as the module's imports, so a name they
392 /// hold is found before the item sets are searched.
393 fn with_imports(mut self, n: usize) -> Resolver {
394 self.imports = n;
395 self
396 }
397 /// The innermost level — the activation a reference resolves against first.
398 fn depth(&self) -> usize {
399 self.levels.len() - 1
400 }
401
402 /// Bind `name`, pushing one cell onto the innermost activation (the matcher
403 /// conses one cell to match) — unless it is a **declared** name, which no
404 /// binder may take ([`check_shadow`](Self::check_shadow)).
405 fn bind(&mut self, name: &Text) -> Result<(), ParseError> {
406 self.check_shadow(name)?;
407 let li = self.depth();
408 self.levels[li].locals.push(name.clone());
409 Ok(())
410 }
411
412 /// **A declared name is fixed over the whole module.** An import, an iota, an
413 /// item and a `local` are declarations, and a declaration's name means one
414 /// thing throughout the scope that declares it, so a binder — a pattern
415 /// binder, a `let` binding, a `recur` binding alike — may not take one.
416 ///
417 /// This runs opposite to [`walk_recur`](Self::walk_recur)'s rejection, which
418 /// stops a group *binding* from shadowing a name already in scope; here an
419 /// outer declaration stops an inner *binder*. Between them a declared name is
420 /// unshadowable from either side.
421 ///
422 /// What a module may still shadow is what it did not declare: the frame names
423 /// a host supplied (a prelude) and the bare builtin aliases (`Int`, `Str`) —
424 /// both ambient, neither visible in the source as a declaration.
425 ///
426 /// The cost is the one the `recur` rule already carries, at a wider radius:
427 /// legality of a body depends on the module's declarations, so adding an
428 /// import or a `const` can break a body that did not change. The trade is that
429 /// fixing a local name is a lexically scoped, mechanically checkable rename,
430 /// while accidental shadowing is silent and reads as working code. See
431 /// `docs/done/2026-08-13_elly-modules-iotas.md`.
432 fn check_shadow(&self, name: &Text) -> Result<(), ParseError> {
433 // The module's imports: the leading root frame slots (the rest of the
434 // frame is the host's, and stays shadowable).
435 if self.levels[0].captures[..self.imports]
436 .iter()
437 .any(|n| n.as_str() == name.as_str())
438 {
439 return Err(ParseError::ShadowsImport(name.clone()));
440 }
441 for members in &self.barriers {
442 if let Some(err) = members.shadow_error(name) {
443 return Err(err);
444 }
445 }
446 Ok(())
447 }
448
449 /// Resolve a reference against level `li` and everything enclosing it, to the de
450 /// Bruijn index it reads *at that level*: a walk over the cells the level's head
451 /// conses, running on into the level's captured frame. A name bound further out
452 /// is captured into every level between it and here (see the module docs), which
453 /// is what builds the capture lists. At the root a name unknown to the frame is
454 /// unbound, or absorbed into it under `auto_bind`.
455 ///
456 /// A failure leaves no trace: the capture is recorded only once the enclosing
457 /// levels have all resolved the name, so an `UnboundName` cannot leave a
458 /// half-captured frame behind.
459 ///
460 /// The module's **items** are searched at the root, after the whole lexical
461 /// stack and before the root frame: an item wins over a frame name, matching
462 /// the inner-before-outer rule everywhere else. An item reports [`Ref::Item`],
463 /// which passes back out through the enclosing levels *without* recording a
464 /// capture — an item is resolved at eval through the terminal the environment
465 /// carries, so it occupies no frame slot. The module's **imports** are the
466 /// leading root frame slots and are searched ahead of the items, for the reason
467 /// [`resolve_module_bodies`] gives.
468 ///
469 /// Scanning every barrier at the root — rather than interleaving each with the
470 /// levels it sits between — is sound because a declared name and a bound one
471 /// are disjoint: an item may not shadow a name already in scope (see
472 /// [`walk_recur`](Self::walk_recur)) and no binder may take a declared one (see
473 /// [`check_shadow`](Self::check_shadow)). So a member name is never found in a
474 /// level's locals at all, and the order the two tiers are scanned in cannot
475 /// change an answer.
476 fn reference(&mut self, name: &str, li: usize) -> Result<Ref, ParseError> {
477 let lvl = &self.levels[li];
478 if let Some(pos) = lvl.locals.iter().rposition(|n| n.as_str() == name) {
479 return Ok(Ref::Local((lvl.locals.len() - 1 - pos) as u32));
480 }
481 if li == 0 {
482 // The module's own imports, ahead of its items: a re-exported import
483 // is both, and its bodies want the frame slot rather than the item
484 // that forwards it (see `resolve_module_bodies`).
485 let root = &self.levels[0];
486 if let Some(slot) = root.captures[..self.imports]
487 .iter()
488 .position(|n| n.as_str() == name)
489 {
490 return Ok(Ref::Local((root.locals.len() + slot) as u32));
491 }
492 // Innermost group first; the distance back to it is the terminal depth
493 // the reference has to walk. Each half is sorted, so a tier is at most
494 // two binary searches.
495 for (i, members) in self.barriers.iter().enumerate().rev() {
496 if let Some(index) = members.member(name) {
497 return Ok(Ref::Item {
498 index,
499 depth: (self.barriers.len() - 1 - i) as u32,
500 });
501 }
502 }
503 // The root frame is reached the same way any level's is: past the
504 // cells this level has consed. Those are usually none, but a
505 // top-level chain abstraction — `(&x …) v`, i.e. every `let` — binds
506 // into the root level rather than opening one of its own.
507 let root = &mut self.levels[0];
508 if let Some(slot) = root.captures.iter().position(|n| n.as_str() == name) {
509 return Ok(Ref::Local((root.locals.len() + slot) as u32));
510 }
511 // The bottom tier: a builtin module under its bare alias. Consulted
512 // after every tier that could *bind* the name — and before the
513 // auto-bind, so an open term reads `Int` as the module rather than
514 // absorbing it into the root frame.
515 if let Some(op) = Builtin::from_alias(name) {
516 return Ok(Ref::Builtin(op));
517 }
518 if !self.auto_bind {
519 return Err(ParseError::UnboundName(Text::from(name)));
520 }
521 let slot = root.captures.len();
522 root.captures.push(Text::from(name));
523 return Ok(Ref::Local((root.locals.len() + slot) as u32));
524 }
525 if let Some(slot) = lvl.captures.iter().position(|n| n.as_str() == name) {
526 return Ok(Ref::Local((lvl.locals.len() + slot) as u32));
527 }
528 // Not known here: resolve it one level out, then capture it into this
529 // level's frame at a fresh slot.
530 let outer = match self.reference(name, li - 1)? {
531 leaf @ (Ref::Item { .. } | Ref::Builtin(_)) => return Ok(leaf),
532 Ref::Local(outer) => outer,
533 };
534 let lvl = &mut self.levels[li];
535 let slot = lvl.captures.len();
536 lvl.captures.push(Text::from(name));
537 lvl.outer.push(outer);
538 Ok(Ref::Local((lvl.locals.len() + slot) as u32))
539 }
540
541 fn walk(&mut self, expr: &mut Expr) -> Result<(), ParseError> {
542 match expr {
543 // Leaves that reference no name.
544 Expr::Int(_) | Expr::Symbol(_) | Expr::Str(_) | Expr::Builtin(_) => Ok(()),
545 // A home-module leaf: the depth to its module is the only thing it
546 // needs, and it is the same barrier-stack distance a `ModItem`
547 // measures — counted from the nearest *module* barrier rather than
548 // from the nearest barrier of either kind.
549 Expr::Home { leaf, depth } => {
550 *depth = Self::home_depth(&self.barriers)
551 .ok_or_else(|| ParseError::OutsideModule(Text::from(leaf.name())))?;
552 Ok(())
553 }
554 // Already resolved: a leaf this pass leaves alone (`ModItem` is produced
555 // by the module resolver itself and is likewise resolved). Resolution
556 // runs **once** per program and is not idempotent — a second pass sees
557 // only `Local`s, finds no free names, and would hand every lambda an
558 // empty capture list while its body still indexes a frame.
559 Expr::Local { .. } | Expr::ModItem { .. } => Ok(()),
560 // Rewrite an in-scope name into a resolved local; the name is kept for
561 // rendering, so the s-expressions are unchanged. A name that is not
562 // lexically bound but is a sibling module item becomes a `ModItem`
563 // (the module tier, consulted only when resolving a module body).
564 Expr::Name(n) => {
565 let name = n.clone();
566 *expr = match self.reference(&name, self.depth())? {
567 Ref::Local(index) => Expr::Local {
568 name: Box::new(name),
569 index,
570 },
571 Ref::Item { index, depth } => Expr::ModItem {
572 name: Box::new(name),
573 index,
574 depth,
575 },
576 Ref::Builtin(op) => Expr::Builtin(op),
577 };
578 Ok(())
579 }
580 Expr::List(elems) => {
581 for e in elems.iter_mut() {
582 self.walk(e)?;
583 }
584 Ok(())
585 }
586 Expr::Map(entries) => {
587 for (k, v) in entries.iter_mut() {
588 self.walk(k)?;
589 self.walk(v)?;
590 }
591 Ok(())
592 }
593 // A call spine. Two argument shapes hold abstractions that the call
594 // *consumes* — they cannot outlive it — so those resolve as
595 // [`Captures::Chain`] (see `consumed_by`):
596 //
597 // - the callee itself, when it is a literal abstraction: `(&x b) v`,
598 // which is every `let`;
599 // - a literal abstraction handed to a builtin that only applies it (an
600 // `__Err.catch` branch, an `__Int.for` callback), including the
601 // elements of a literal clause list (`__match [&p e, …] x`).
602 Expr::App(items) => {
603 let callee = &mut items[0];
604 match callee {
605 Expr::Abs(code) => self.walk_abs(code, Captures::Chain)?,
606 _ => self.walk(callee)?,
607 }
608 // `__Int.add x y` — a builtin module applied to a literal member
609 // symbol — folds to the member's own leaf, dropping the symbol from
610 // the spine, so the written form costs exactly what the old flat
611 // `__Int.add` did. The alias spelling (`Int.add`) folds too: the
612 // callee walk above has just rewritten `Int` to its module builtin.
613 //
614 // Before the arguments are walked, because both of the things that
615 // depend on the callee's builtin — which arguments it *applies*
616 // (`applies_arg`, below) and where those arguments sit — are the
617 // member's, not the module's.
618 if let (Expr::Builtin(ns), Some(Expr::Symbol(m))) = (&items[0], items.get(1)) {
619 if let Some(op) = ns.member(m) {
620 *expr = fold_member(items, op);
621 return self.walk(expr);
622 }
623 }
624 let (callee, args) = items.split_first_mut().expect("a call spine has a callee");
625 // The callee's builtin, if it is one: what consumes the argument
626 // shapes below.
627 let consumer = match callee {
628 Expr::Builtin(op) => Some(*op),
629 _ => None,
630 };
631 for (i, arg) in args.iter_mut().enumerate() {
632 match (consumer, &mut *arg) {
633 (Some(op), Expr::Abs(code)) if op.applies_arg(i) => {
634 self.walk_abs(code, Captures::Chain)?
635 }
636 (Some(op), Expr::List(elems)) if op.applies_elements_of(i) => {
637 for e in elems.iter_mut() {
638 match e {
639 Expr::Abs(code) => self.walk_abs(code, Captures::Chain)?,
640 _ => self.walk(e)?,
641 }
642 }
643 }
644 _ => self.walk(arg)?,
645 }
646 }
647 Ok(())
648 }
649 // An abstraction reached anywhere else may escape, so it captures.
650 Expr::Abs(code) => self.walk_abs(code, Captures::Frame(Box::new([]))),
651 Expr::Recur(r) => self.walk_recur(r),
652 }
653 }
654
655 /// How many terminals a [home-module leaf](crate::HomeLeaf) written here has
656 /// to **step over** to reach its module: the barriers are scanned
657 /// innermost-first for the nearest one written as a module, and the answer is
658 /// the number of groups passed on the way.
659 ///
660 /// `None` is "outside a module body", and the one test covers both ways of
661 /// being outside: a term with no barriers at all, and a `recur` in a program
662 /// that is not a module, where every barrier on the stack is a group.
663 ///
664 /// Written as a search rather than a test on the outermost barrier because
665 /// anonymous modules (`docs/todo/elly-modules-anon.md`) will make a module
666 /// barrier reachable anywhere on the stack — and because the search says what
667 /// the rule *is*.
668 fn home_depth(barriers: &[Barrier]) -> Option<u32> {
669 barriers
670 .iter()
671 .rev()
672 .position(|b| matches!(b, Barrier::Module { .. }))
673 .map(|d| d as u32)
674 }
675
676 /// Is `name` reachable from here without binding anything new? A read-only
677 /// probe over the lexical stack, the enclosing item sets and the root frame —
678 /// it records no capture and never auto-binds, so asking is free of side
679 /// effects (unlike [`reference`](Self::reference), which builds capture lists
680 /// as it goes).
681 fn in_scope(&self, name: &str) -> bool {
682 let lexical = self.levels.iter().any(|lvl| {
683 lvl.locals.iter().any(|n| n.as_str() == name)
684 || lvl.captures.iter().any(|n| n.as_str() == name)
685 });
686 lexical || self.barriers.iter().any(|m| m.member(name).is_some())
687 }
688
689 /// Resolve a [`recur`](Expr::Recur): its group becomes a barrier, its binding
690 /// bodies and then its body resolve under it.
691 ///
692 /// **A binding may not shadow a name already in scope.** `recur`'s bindings are
693 /// simultaneous, so inside the group a shadowed name means the group's own
694 /// binding, never the outer one — `recur (x = f x) …` under an enclosing `x`
695 /// self-references where the identical `let` line reads the outer value. The two
696 /// forms would look alike and mean opposite things, so the shadow is refused
697 /// ([`ParseError::RecurShadowsOuter`]) rather than silently resolved. The cost
698 /// is that legality depends on the whole enclosing scope: a new import or
699 /// prelude name can break an inner `recur` that did not change.
700 ///
701 /// The reverse direction is refused too, by the rule covering every binder:
702 /// a binder *inside* the group may not take a binding's name either (see
703 /// [`check_shadow`](Self::check_shadow)). Together the two make a group's
704 /// names mean one thing throughout it.
705 ///
706 /// No level is pushed. A barrier is not an activation — nothing is bound by
707 /// crossing the terminal and nothing is captured through it, so the group's
708 /// bodies keep counting into the enclosing level's chain and land on the same
709 /// indices they would have had inline at the `recur` site.
710 fn walk_recur(&mut self, group: &mut Rc<ModuleData>) -> Result<(), ParseError> {
711 let names: Vec<Text> = group.items().iter().map(|(n, _)| n.clone()).collect();
712 for name in &names {
713 if self.in_scope(name) {
714 return Err(ParseError::RecurShadowsOuter(name.clone()));
715 }
716 }
717 self.barriers.push(Barrier::Group { items: names });
718 let data = Rc::get_mut(group).expect("the group is uniquely owned at resolve time");
719 for (_, body) in data.items_mut() {
720 self.walk(body)?;
721 }
722 // The bindings are walked before the body, matching the order the evaluator
723 // binds in: a chain abstraction in the body extends this level's locals, and
724 // must not be visible to the bodies that precede it.
725 let body = data.body_mut().expect("a recur group always has a body");
726 self.walk(body)?;
727 // An error aborts the whole pass (the resolver is dropped with it), so the
728 // stack need not be unwound on the way out.
729 self.barriers.pop();
730 Ok(())
731 }
732
733 /// Resolve an abstraction, `how` selecting the environment its closures get.
734 ///
735 /// [`Captures::Frame`] opens a **new activation level** for the head's
736 /// parameters: resolve each parameter in binding order (so a later parameter's
737 /// embedded expression sees the earlier ones), then the body; popping the level
738 /// yields the free variables the walk found, in frame-slot order and addressed in
739 /// the *enclosing* activation's coordinates — the plan the evaluator reads to
740 /// build the frame. (The plan passed in is ignored; only the variant selects.)
741 ///
742 /// [`Captures::Chain`] opens **no** level: the head's binders extend the
743 /// enclosing activation, and the body's references keep counting into its chain —
744 /// exactly what the evaluator does when it conses this closure's arguments onto
745 /// the environment it shares. Nothing is captured, so nothing is copied.
746 fn walk_abs(&mut self, code: &mut Rc<Lambda>, how: Captures) -> Result<(), ParseError> {
747 let lam = Rc::get_mut(code).expect("the lambda is uniquely owned at resolve time");
748 if let Captures::Chain = how {
749 let li = self.depth();
750 let base = self.levels[li].locals.len();
751 for pat in lam.head.iter_mut() {
752 self.walk_pattern(pat)?;
753 }
754 self.walk(&mut lam.body)?;
755 self.levels[li].locals.truncate(base);
756 lam.captures = Captures::Chain;
757 return Ok(());
758 }
759 self.levels.push(Level::new());
760 for pat in lam.head.iter_mut() {
761 self.walk_pattern(pat)?;
762 }
763 // An error aborts the whole pass (the resolver is dropped with it), so the
764 // level stack need not be unwound on the way out.
765 self.walk(&mut lam.body)?;
766 let level = self.levels.pop().expect("the level just pushed");
767 lam.captures = Captures::Frame(Self::capture_plan(&level.outer));
768 Ok(())
769 }
770
771 /// Turn a finished level's captures into the lambda's frame-building plan:
772 /// `outer[slot]` is where slot `slot` reads from, and the plan is **sorted by
773 /// `outer`** so the evaluator fills the frame in one ascending walk of the
774 /// defining environment. The outer indices are distinct (a name is captured into
775 /// a level at most once), so the order is total and the sort deterministic.
776 fn capture_plan(outer: &[u32]) -> Box<[Capture]> {
777 let mut plan: Vec<Capture> = outer
778 .iter()
779 .enumerate()
780 .map(|(slot, &outer)| Capture {
781 outer,
782 slot: slot as u32,
783 })
784 .collect();
785 plan.sort_unstable_by_key(|c| c.outer);
786 plan.into_boxed_slice()
787 }
788
789 /// Resolve a pattern: push its binders (assigning their de Bruijn positions)
790 /// and resolve its embedded expressions, in the exact order the evaluator's
791 /// matcher conses.
792 fn walk_pattern(&mut self, pat: &mut Pattern) -> Result<(), ParseError> {
793 match pat {
794 Pattern::Discard => Ok(()),
795 Pattern::Bind(name) => self.bind(name),
796 // `name = <pat>` conses the name before matching the inner pattern.
797 Pattern::At(name, inner) => {
798 self.bind(name)?;
799 self.walk_pattern(inner)
800 }
801 // Comparands see the bindings made before them (no new binding).
802 Pattern::Equal(e) | Pattern::Less(e) | Pattern::Greater(e) => self.walk(e),
803 // `<pat> as <Proto>` narrows, then matches the inner pattern. The
804 // prototype reference is resolved **before** the inner pattern, the
805 // order an `= <ref>` comparand uses and for the same reason: it sees
806 // only the bindings made before it.
807 //
808 // A reference that lands on a builtin module is folded to the
809 // compiled form here, where both spellings have arrived — the written
810 // `__Int` from parse, and the alias `Int` from the walk just above.
811 Pattern::Type(proto, inner) => {
812 if let ProtoRef::Ref(expr) = proto {
813 self.walk(expr)?;
814 if let Expr::Builtin(op) = expr {
815 match op.proto_kind() {
816 Some(kind) => *proto = ProtoRef::Kind(kind),
817 None => return Err(ParseError::NotAPrototype(Text::from(op.name()))),
818 }
819 }
820 }
821 self.walk_pattern(inner)
822 }
823 // `__value <pat>` takes the same depth the leaf does, and holds no
824 // reference of its own — the module is implicit.
825 Pattern::Unwrap { depth, inner } => {
826 *depth = Self::home_depth(&self.barriers)
827 .ok_or_else(|| ParseError::OutsideModule(Text::from("__value")))?;
828 self.walk_pattern(inner)
829 }
830 // Both arms must cons the same binding sequence — same names, same
831 // order — so a later reference reads one fixed index whichever arm
832 // matched. Walk the left arm, capture the names it pushed, reset, walk
833 // the right arm, and require the sequences to be equal.
834 Pattern::Or(left, right) => {
835 let li = self.depth();
836 let base = self.levels[li].locals.len();
837 self.walk_pattern(left)?;
838 let left_names: Vec<Text> = self.levels[li].locals[base..].to_vec();
839 self.levels[li].locals.truncate(base);
840 self.walk_pattern(right)?;
841 if self.levels[li].locals[base..] != left_names[..] {
842 return Err(ParseError::OrBindersMismatch);
843 }
844 Ok(())
845 }
846 // A list pattern conses its rest first, then its elements left to right.
847 Pattern::List(elems, rest) => {
848 if let Rest::Named(name) = rest {
849 self.bind(name)?;
850 }
851 for e in elems.iter_mut() {
852 self.walk_pattern(e)?;
853 }
854 Ok(())
855 }
856 // A map pattern evaluates every lookup key first (in the scope before
857 // any of this pattern's bindings), then matches lookup value patterns,
858 // then captures (key then value), then conses a named rest last.
859 Pattern::Map(entries, rest) => {
860 for (key, _) in entries.iter_mut() {
861 if let MapKey::Lookup(ke) = key {
862 self.walk(ke)?;
863 }
864 }
865 for (key, vpat) in entries.iter_mut() {
866 if let MapKey::Lookup(_) = key {
867 self.walk_pattern(vpat)?;
868 }
869 }
870 for (key, vpat) in entries.iter_mut() {
871 if let MapKey::Capture(kpat) = key {
872 self.walk_pattern(kpat)?;
873 self.walk_pattern(vpat)?;
874 }
875 }
876 if let Rest::Named(name) = rest {
877 self.bind(name)?;
878 }
879 Ok(())
880 }
881 }
882 }
883}