Ichor.Toolkit.Scope (Ichor v0.2.1)

Copy Markdown View Source

A generic nested lexical scope / symbol table: a stack of name->value bindings, innermost scope first, supporting the "define here, look up through enclosing scopes" shape almost every real language's semantic analysis needs (variable resolution, nested function/block scoping, ...). No existing internal precedent to extract this from (unlike Ichor.Toolkit.Fixpoint/Graph) -- a from-scratch design, but a standard, well-understood one.

Deliberately minimal: define/3 never itself rejects a duplicate key in the same scope. Whether redefining a name within one scope is allowed is a real per-language policy choice (some languages reject it, some don't) -- left to the caller, via lookup_local/2, rather than baked in here. Nothing is grammar-specific; key can be any term with a working ==/2, not just an atom.

Summary

Functions

Binds key to value in the current (innermost) scope, shadowing any same-named binding in an enclosing one.

Looks up key, searching from the innermost scope outward. :error if it's not bound anywhere.

Looks up key in the current (innermost) scope only, not any enclosing one -- what a caller wanting to reject same-scope redefinition checks before calling define/3.

A fresh scope with just the outermost (global) level.

Exits the current (innermost) scope, discarding its own bindings. Raises (FunctionClauseError) if there's no nested scope left to pop -- the outermost level, from new/0, is never popped, the same way popping an empty stack is a caller bug, not a recoverable condition.

Enters a new, empty nested scope.

Types

t()

@opaque t()

Functions

define(list, key, value)

@spec define(t(), term(), term()) :: t()

Binds key to value in the current (innermost) scope, shadowing any same-named binding in an enclosing one.

lookup(scope, key)

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

Looks up key, searching from the innermost scope outward. :error if it's not bound anywhere.

lookup_local(list, key)

@spec lookup_local(t(), term()) :: {:ok, term()} | :error

Looks up key in the current (innermost) scope only, not any enclosing one -- what a caller wanting to reject same-scope redefinition checks before calling define/3.

new()

@spec new() :: t()

A fresh scope with just the outermost (global) level.

pop(scope)

@spec pop(t()) :: t()

Exits the current (innermost) scope, discarding its own bindings. Raises (FunctionClauseError) if there's no nested scope left to pop -- the outermost level, from new/0, is never popped, the same way popping an empty stack is a caller bug, not a recoverable condition.

push(scope)

@spec push(t()) :: t()

Enters a new, empty nested scope.