# Context

`Context` is a name-to-[Value](value.md) symbol table; `ContextStack` is a stack of `Context`s with scope-like lookup, plus the process-wide registries of builtin functions/operators and named constants. Together they supply the variable and function bindings the [RPN evaluator](rpneval.md) resolves `Rpn::Identifier` and `Rpn::FunctionCall` entries against. Both are declared in `context.h`; the static tables and stack logic are in `context.cpp`.

## Context

A `Context` is little more than an `unordered_map<Id, Value>` (see [identifiers](identifier.md)), buildable in one shot from a `{name, value}` initializer list - the way `ContextStack::consts` below is populated. `insert()` reports whether the name was newly added or replaced an existing binding; `insertAndCheck()` additionally compares the old and new `Value` with `operator!=` and reports whether the value actually *changed* - used by callers that need to know whether a parameter update invalidates anything derived from it, not just whether an assignment happened. `get()` returns a pointer, `nullptr` if the name is unbound. `dump()` prints every binding as `name = value`; it backs the control block's `print variables` (see `simulator/cmd.cpp`, which calls it directly on the evaluator's outermost context).

`Context::insert`/`insertAndCheck` themselves have no notion of constants - that guard lives one layer up, in `ContextStack` (below). Code that owns and writes a raw `Context` directly, bypassing `ContextStack`, is responsible for its own rules: `Circuit`'s `.var` circuit-variable assignment (`cirvariables.cpp`) writes straight through `Context::insertAndCheck()` on the `Context` it owns, rather than going through `ContextStack::insert()`.

## ContextStack

`ContextStack::stack` holds `std::variant<Context, Context*>` entries, so a stack level can either own a freshly-created `Context` or wrap a pointer to one owned elsewhere. `enter(external, addToPath)` is used both ways: called with no argument it pushes a new empty owned `Context` (a fresh local scope, e.g. a hierarchical instance's parameter scope being rebuilt); called with a pointer it pushes a *reference* to a `Context` that outlives the stack frame and is shared with other code - for example the circuit's toplevel `.var` variables `Context`, which `Circuit` itself owns and which the parameter evaluator's stack only points into (see `cirvariables.cpp`).

### Lookup: top scope, search path, constants, custom resolver

`get(name)` tries, in order:

1. The context at the *top* of the stack - the innermost/current scope.
2. Each context recorded in `searchPath`, most recently added first (skipping the top context if it is already the one on the path, to avoid searching it twice).
3. The static `consts` table (see below).
4. A pluggable `Resolver*` installed via `installCustomResolver()`, for namespaces that aren't ordinary `Context`s at all (e.g. `circuit.cpp` installs a resolver that exposes simulator options as if they were variables).

`searchPath` exists because subcircuit hierarchy nesting and lexical variable visibility are not the same thing: pushing one `Context` per level of instance hierarchy would normally only make the *immediately enclosing* scope visible, but an expression deep inside a subcircuit instance still needs to see, for instance, the top-level circuit's `.var` variables directly. `enter(..., addToPath=true)` marks a context as reachable from every stack level pushed after it, regardless of nesting depth, instead of requiring it to be re-threaded through every intermediate scope.

### Entering and exiting

`exit()` pops one level (refusing to pop the last, global level) and also pops it from `searchPath` if it was the top of that too. `exit(stackMarker)` truncates the stack back to a previously recorded depth in one step - `RpnEvaluator::contextMarker()`/`revertContext()` use this to roll back every context entered since a checkpoint when hierarchy descent aborts partway through (see `HierarchicalInstance::enterContext` in `hierdevice.cpp`), rather than requiring the caller to `exit()` once per level pushed.

### Inserting

`ContextStack::insert()`/`insertAndCheck()` write into the *top* context only, and first check `isConstant(name)`: names in `consts` can never be shadowed by a netlist variable or parameter, so attempting to redefine one fails with `Status::Redefinition` instead of silently masking a builtin constant (see [Status](status.md) for the return-value/`Status&` convention).

## Constants

`ContextStack::consts` is a static `Context` populated with `Id::createStatic(...)` names (see [identifiers](identifier.md) for why these must be static-linkage globals constructed before any ordinary identifier): the standard math constants (`M_PI`, `M_E`, ...) and physical constants (`P_Q`, `P_C`, `P_K`, ...). `isConstant(name)` is just membership in this table.

## Builtins

`Builtin` pairs an `RpnBuiltinFunc` (`bool(RpnStack&, Rpn::Arity, RpnEvaluationNetlistContext&, Status&)`, see [the RPN stack](rpnstack.md) and [evaluator's netlist context](rpnevalctx.md)) with `minArity`/`maxArity` and a `pure` flag. `ContextStack::builtins` is the master name-to-function registry for every builtin operator wrapper and function documented in [functors](rpnfunctor.md) and [builtin operators and functions](rpnbuiltin.md); `getBuiltin(name)` is its lookup.

`pure` marks whether repeated calls with the same arguments always produce the same result. Every ordinary math/type/vector function is `pure`; the Monte Carlo generators (`agauss`, `gauss`, `aunif`, `unif`) are not, since they draw a fresh (memoized-per-sample) random value. `RpnEvaluator::isConstant()` (see [Evaluator](rpneval.md)) uses this flag - together with `isConstant()` on any identifiers referenced - to decide whether a whole expression's value can be computed once and reused instead of re-evaluated on every access.
