Skip to main content

resolve_module_bodies

Function resolve_module_bodies 

Source
pub fn resolve_module_bodies(
    bindings: &mut [(Text, Expr)],
    locals: &mut [(Text, Expr)],
    main: Option<&mut Expr>,
    imports: &[Text],
    iotas: &[Text],
    frame: &[Text],
) -> Result<(), ParseError>
Expand description

Resolve each body of a module’s bindings in place (name → de Bruijn Local, sibling item → Expr::ModItem, else ParseError::UnboundName). The item names form an extra reference tier consulted after lexical scope and before the unbound check, so a body may refer to any sibling regardless of source order — the basis of order-independent, mutually recursive top-level definitions (see docs/done/2026-08-07_elly-modules-v0.md). Called by crate::compile_module after crate::parse::parse_module.

The bindings are first sorted by name: that order is the module’s item table, and a sibling reference records its position there as the ModItem index, so neither this pass nor eval compares names down a list. parse_module rejects duplicates, so the names are distinct and the order is total.

A name in a module body is looked for in this order: the enclosing &/let binders, then the module’s imports, then its own members — iotas before items and locals — then the rest of the frame, then the builtin module aliases. Anything found in the frame — imports and the outer values alike — becomes an ordinary Local whose index walks past the module terminal into the frame, so it costs one indexed lookup and evaluates nothing.

locals are the module’s local declarations: members exactly as the items are — so a body reaches one the same way, and a local’s own body is resolved here alongside the items’ — differing only in being addressed after the items, which is where an outside M.name access stops looking. They are sorted here as the bindings are, and the two orders concatenated are the module’s id-bearing address space.

main is the module’s __main, when a source declared one: the reserved moditem a runner evaluates. It is resolved here, with the item bodies and against the same tiers, because it is written in the module’s own scope. It is not a member, so it claims no index and shifts nothing — the member address space is exactly what it was before a source grew one.

imports names the module’s own imports and frame whatever the compile was given (a host prelude); together, imports first, they are the frame the module is instantiated against. iotas names its const declarations, which take no slot: they are members addressed past the items and the locals, so an iota’s ModItem index is items.len() + locals.len() + its position. Pass empty slices for a module with none of them.

Iotas are looked for before items for the reason imports are: the re-exporting form false = const is one declaration yielding both an iota and an item forwarding it, and a sibling reference wants the iota rather than the item that would evaluate to it.

Imports are looked for before items because of the re-exporting form Foo = import "spec", which is one declaration that yields both a frame slot and an item forwarding it. Searching items first would send every body’s Foo through the forwarding item, evaluating it to reach the slot the body wanted; finding the slot directly means each reference reads the one instance and the item is left for M.Foo from outside. No other name can be both, since parse_module rejects an import and an item that share one.