Skip to main content

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}