Logos.Env (Logos v0.2.0)

Copy Markdown

Lexical scope, and only lexical scope. This is a plain immutable chained-frame environment: a frame is a bindings map plus a parent frame (or nil at the root). let-style lexical binding and fn closures are the only things this module is responsible for -- top-level def visibility is an entirely separate concern, handled by Logos.Namespace/Logos.Var against a Logos.Runtime (see Logos.Eval's moduledoc for how symbol resolution combines the two).

This module deliberately knows nothing about primitives, def, or namespaces -- it is pure lexical-scope bookkeeping, nothing else.

Summary

Functions

Adds (or overwrites) name -> value in env's own frame and returns the updated frame -- it does not create a child frame.

Every name bound anywhere in env's frame chain (own frame plus every ancestor), as a MapSet.t(String.t()) -- used to seed Logos.Macroexpand's lexical-shadow tracking (the locals set it threads through a form, so it never mistakes a shadowed local for a macro reference) from whatever top-level lexical continuity a caller (e.g. a REPL threading env across calls) already has, rather than starting macroexpansion's shadow-set from scratch and missing bindings that are real at eval time.

Walks env's frame chain (own frame first, then parent, then parent.parent, ...) looking for name. {:ok, value} on a hit, :error once the chain is exhausted.

A fresh, empty root frame (no parent).

A fresh, empty frame chained under parent -- e.g. a new lexical scope opened by fn/let.

Types

t()

@type t() :: %Logos.Env{
  bindings: %{required(String.t()) => term()},
  parent: t() | nil
}

Functions

bind(env, name, value)

@spec bind(t(), String.t(), term()) :: t()

Adds (or overwrites) name -> value in env's own frame and returns the updated frame -- it does not create a child frame.

Design choice: bind/3 extends the frame it's given rather than implicitly nesting a new one, so that binding N params/let clauses into the same new scope is a simple fold (Enum.reduce(pairs, Env.new(parent), fn {k, v}, env -> Env.bind(env, k, v) end)) that only ever allocates one new frame, not N nested ones. Callers who want a new scope call new/1 explicitly first, then bind/3 into it.

bound_names(env)

@spec bound_names(t()) :: MapSet.t(String.t())

Every name bound anywhere in env's frame chain (own frame plus every ancestor), as a MapSet.t(String.t()) -- used to seed Logos.Macroexpand's lexical-shadow tracking (the locals set it threads through a form, so it never mistakes a shadowed local for a macro reference) from whatever top-level lexical continuity a caller (e.g. a REPL threading env across calls) already has, rather than starting macroexpansion's shadow-set from scratch and missing bindings that are real at eval time.

lookup(env, name)

@spec lookup(t(), String.t()) :: {:ok, term()} | :error

Walks env's frame chain (own frame first, then parent, then parent.parent, ...) looking for name. {:ok, value} on a hit, :error once the chain is exhausted.

new()

@spec new() :: t()

A fresh, empty root frame (no parent).

new(parent)

@spec new(t()) :: t()

A fresh, empty frame chained under parent -- e.g. a new lexical scope opened by fn/let.