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

Immutable-ish environment for the Scheme evaluator.

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 maps; new frames are
pushed by `extend/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 are stored as a Map on the process heap, 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.

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.

## Recursive lexical frames

`letrec`, `letrec*`, named `let`, and the `letrec*` produced by
internal-define splicing each push a *recursive* frame. A recursive
frame is a map of name → value backed by a process-dictionary slot
keyed by `make_ref/0`, 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()} | {: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.

# `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*
