# `Schooner.Env`
[🔗](https://github.com/ausimian/schooner/blob/1.2.0/lib/schooner/env.ex#L1)

Environment for the Scheme evaluator: immutable lexical frames over
a mutable globals table.

An environment is a chain of lexical frames innermost-first plus a
*globals* slot identified by a process-dictionary key (a fresh
reference). Lexical frames are immutable; new frames are pushed by
`extend/2` (or, inside the evaluator, `push_frame/2`). The globals
slot is mutable in the strict sense — `define/3` writes to it — so
closures that captured an env see top-level definitions added after
they were created. This is the standard `letrec`-style knot-tying
applied to the top-level frame: closures reference the frame by
identity, not by value-at-bind-time.

Globals live in the process dictionary, not in `:ets`, so
consecutive lookups of the same name return the *same* heap term.
This preserves `erts_debug.same/2` identity for aggregates bound
at top level — `(eq? x x)` answers `#t` for vectors, pairs,
parameters, and other aggregates, matching the lexical case.

Each global has its own *cell*: a process-dictionary entry keyed by
a fresh reference, holding the value. The globals slot maps names to
cells. The evaluator resolves a global reference to its cell once,
when it compiles the reference, so reading a global at run time is a
single process-dictionary get rather than a lookup by name. A cell
is created the first time a name is defined or compiled as a
reference, and holds an "unbound" sentinel until it is defined.

The globals slot is owned by the process that calls `new/0` and is
reclaimed when that process exits, so per-execution sandboxing
falls out of the BEAM's process model.

## Frame shapes

The evaluator resolves every lexical variable reference to a
`{depth, slot}` address when it analyses the program, before it
runs, so the frames it pushes are *positional*:
a tuple whose element 0 is a tuple of the frame's names and whose
remaining elements are the values in the same order. The names are
carried so `lookup/2` can still resolve by name; the evaluator
itself only indexes.

`extend/2` pushes a name → value map frame instead. Both shapes, and
the recursive frames below, are understood by `lookup/2`.

## Recursive lexical frames

`letrec`, `letrec*`, named `let`, and the `letrec*` produced by
internal-define splicing each push a *recursive* frame, unless the
evaluator has compiled the form to direct calls or proved that its
targets are distinct and every initializer reference to the same
frame targets an earlier binding, including references inside nested
procedures. Those forms use immutable positional frames: init
closures capture the earlier values, and body closures capture the
fully initialized frame. These snapshots live as long as the closures
that reference them and need no process-dictionary slot or cleanup.

Duplicate targets and initializer references to their own or later
bindings still need recursive frames unless compiled to direct calls.
A recursive frame is a process-dictionary slot keyed by `make_ref/0`,
holding the frame's names and a tuple of values, so closures captured
during init evaluation see later bindings via frame identity.

Each such frame is released by `release_rec/1` when the body
finishes — *unless* the body's return value carries a closure whose
env still names the frame. In that case the evaluator detects the
escape and leaves the slot alive holding the now-finalised
snapshot, so the escaped closure (and any closures it reaches via
rec lookups, including mutually-recursive bindings) can resolve
rec names through the slot for the rest of the process lifetime.
Slot leakage is therefore bounded to *actual* closure escapes — a
letrec form whose body returns a non-closure value (the dominant
case in typical Scheme code, including all named-let recursion
whose body is `(tag val ...)`) releases its slot as usual.

# `frame`

```elixir
@type frame() ::
  %{optional(binary()) =&gt; Schooner.Value.t()} | tuple() | {:rec, reference()}
```

# `lookup_result`

```elixir
@type lookup_result() ::
  {:ok, Schooner.Value.t()} | :error | {:uninitialised, binary()}
```

# `t`

```elixir
@type t() :: %Schooner.Env{globals: reference(), lex: [frame()]}
```

# `define`

```elixir
@spec define(t(), binary(), Schooner.Value.t()) :: t()
```

Add or replace a top-level binding. Visible to every closure that
captured this env, regardless of when it was created.

# `extend`

```elixir
@spec extend(t(), [{binary(), Schooner.Value.t()}]) :: t()
```

Push a new lexical frame on top of `env`.

# `extend_rec`

```elixir
@spec extend_rec(t(), [binary()]) :: t()
```

Push a fresh `letrec`-style frame whose bindings start uninitialised
and can be assigned later via `rec_set/3`. Closures created against
the returned env see the bindings by frame identity, so forward
references resolve once the frame has been populated. Backed by a
process-dictionary slot keyed by a fresh reference; the slot must
be released with `release_rec/1` once the binding form's body has
finished evaluating.

Duplicate names share one slot.

# `lookup`

```elixir
@spec lookup(t(), binary()) :: lookup_result()
```

Look up `name`. Walks lexical frames innermost-first, then globals.

Returns `{:uninitialised, name}` when `name` is bound by an enclosing
recursive frame whose init expression has not yet been evaluated;
callers should treat this as a forward-reference runtime error.

# `new`

```elixir
@spec new() :: t()
```

# `pop`

```elixir
@spec pop(t()) :: t()
```

Pop the topmost lexical frame on `env`.

# `rec_set`

```elixir
@spec rec_set(t(), binary(), Schooner.Value.t()) :: t()
```

Set a binding in the topmost recursive frame on `env`.

# `release_rec`

```elixir
@spec release_rec(t()) :: :ok
```

Release the topmost recursive frame on `env`, deleting the
process-dictionary slot that backs it. Must be paired with
`extend_rec/2`. Idempotent — releasing a frame whose slot was
already deleted is a no-op.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
