pub enum EnvNode {
Cons {
val: Value,
next: Env,
},
Module {
data: Rc<ModuleData>,
id_base: u64,
frame: Env,
},
Frame {
vals: Box<[Value]>,
home: Env,
},
}Expand description
One link of the environment list: a single binding carrying its val inline,
or the frame that terminates the walk. The empty environment is None, not a
node — Env is an Option<Rc<EnvNode>>, which the null-pointer niche keeps
one word wide, so ending a chain costs no allocation and no refcount traffic. A lambda activation conses one Cons
per name its head binds — zero for a binder-less head (&_, &(< 2)) — so a
partial application’s environment is just the consed prefix, shared with later
partials by Rc refcount (no copy-on-write). Names are not stored: references
were resolved to a de Bruijn index against this list’s shape.
Variants§
Cons
Module
The home terminal of a module item’s environment: it carries the item’s
module in place of the empty terminal, so a Expr::ModItem sibling
reference resolves
by walking out to it (see the ModItem arm of eval_env). It binds no
name and is not counted by de Bruijn Local indices — a valid index always
lands on a Cons or a Frame before reaching here. A materialized closure
captures an environment ending in this node, which is how it keeps its module
alive (an Rc edge closure → module, with no edge back — see
docs/done/2026-08-07_elly-modules-v0.md).
This node is the module instance: Value::Module is a handle on it,
minted by instantiate. So the value and the environment its items run
in are one allocation, and M.name from outside runs the body in exactly
the environment a sibling reference would.
id_base is the module’s reserved id block: item i stamps the closure it
materializes to with id_base + i, so M.f == M.f (see
materialize_body).
frame is the environment the module was instantiated against — its
imports, host prelude and module-minted identities, in the slot order
ModuleData::frame_names fixed at compile time. A de Bruijn index walks
through this node into frame without counting it, so a frame
reference is an ordinary Expr::Local and needs no variant of its own.
The node binds no name itself, which is what makes passing the walk on
consistent rather than an off-by-one.
The shape of frame is deliberately left open: it is an Env, so a flat
Frame (named imports — a fixed slot list) and a cons
chain (recur — the frame is the enclosing lexical environment) are both
reachable through one code path. None is a module with no frame, which is
every module compiled by crate::compile_module.
Holding an environment is what gives up modules v0’s structural
acyclicity — a ModuleData could hold no Value, so no cycle was
expressible — for a
temporal one: a frame holds only values that existed before the frame did,
and immutability means none of them can later be made to point back at it.
See docs/done/2026-08-13_elly-modules-env.md.
Frame
A closure’s captured frame: the values of the free variables its body
actually uses, copied in at closure creation in the slot order
Lambda::captures fixed at resolve. It terminates
the de Bruijn walk: an index that runs off the consed cells selects
vals[remaining] (see lookup), so a reference into an enclosing scope
costs a walk over the activation’s own bindings plus one array index —
never a walk over the whole enclosing program — and a closure retains only
what it uses.
home carries the environment’s terminal (None, or the
Module node of
the module item this closure came from) on past the frame, so a ModItem
inside the body still finds its module — and finds the same node, so
materializing an item allocates no environment. Nothing addresses home by
index: the frame is the last indexable node.
The values live inline behind the environment’s own Rc, so a frame is one
allocation and is shared by refcount exactly as the captured cons tail used
to be.