# Evaluator

`RpnEvaluator` (`rpneval.h`/`rpneval.cpp`) ties together a compiled [expression](rpnexpr.md), the [value stack](rpnstack.md), the [variable/function context](context.md), and the [Monte Carlo netlist context](rpnevalctx.md), and runs the actual stack-machine interpreter loop.

## State it owns

An `RpnEvaluator` owns one `RpnStack` and one `ContextStack`, plus a raw, non-owning `MCData*`. Owning these (rather than taking them as parameters) lets both persist across many separate `evaluate()` calls - the context stack in particular needs to retain variable scopes entered by earlier evaluations (e.g. a subcircuit's parameter scope, see [Context](context.md)) without being torn down and rebuilt each time. `contextMarker()`/`revertContext(marker)` expose the underlying `ContextStack::depth()`/`exit(stackMarker)` as a checkpoint/rollback pair, used by callers such as hierarchical-instance context building (`hierdevice.cpp`) to unwind every context entered since a checkpoint when an error aborts partway through descending the instance hierarchy. `stack()`/`contextStack()` expose the owned objects directly, e.g. for the control block's `print variables` command to walk and `dump()` the outermost context (see [Context](context.md)).

Because all of this state is owned per-evaluator rather than global, a program can run several independent evaluators side by side. `Circuit` keeps exactly two: `paramEvaluator_`, which resolves instance/model parameter expressions against the hierarchical scopes described in [Context](context.md), and `variableEvaluator_`, which resolves the circuit's top-level `.var` variables and control-block expressions against a separate, single-level context. Each has its own stack, context stack, and Monte Carlo wiring, so evaluating one never disturbs the other's in-progress state.

## `evaluate()`

`evaluate(rpn, result, ctx, s)` clears the stack, wires `mcData_` into `ctx` and resets its call-index counter (see [the netlist context](rpnevalctx.md)), then walks `rpn` entry by entry with an explicit loop rather than a range-based `for` - `TJump`/`TBranch` entries need to move the iterator by an arbitrary signed offset instead of always advancing by one, so an `isJump` flag suppresses the trailing `++e` exactly when a jump/branch already repositioned it.

Per entry type:

- **`TValue`**: pushes a copy of the literal.
- **`TIdentifier`**: resolves the name via `ContextStack::get()`; `Status::NotFound` plus the entry's source location if unbound.
- **`TOp`**: dispatches each `OpCode` to the matching generic wrapper from [builtin operators and functions](rpnbuiltin.md) (`mathFuncComp1/2`, `mathRelOpComp2`, `mathBitOpComp1/2`, `mathLogicOp1`, `mathFuncSelector2`). The whole dispatch runs inside a `try`/`catch` that turns any stray C++ exception into `Status::Exception` instead of letting it propagate - a bug surfacing deep inside a functor becomes a reported error attached to the offending expression's location, not a crash of the whole simulator run. `OpAnd`, `OpOr`, and `OpQuestion` are deliberate no-ops here (`coreSt = true`): the actual short-circuit control flow already happened via the compiled `Branch`/`Jump` entries (see [expression format](rpnexpr.md)) - these `Op` entries exist only so `Rpn::str()` can reprint `&&`/`||`/`?:`.
- **`TFunctionCall`**: looks up the name via `ContextStack::getBuiltin()`, checks `arity` against `[minArity, maxArity]`, then calls `builtin->func(stack_, arity, ctx, s)`, reporting `NotFound`/`BadArguments` with location on failure.
- **`TPackVec`/`TPackList`**: delegate to `vectorPack`/`listPack` (see [builtin operators and functions](rpnbuiltin.md)), building `[a, b, c]` vector and `{a, b, c}` list syntax respectively (each also has a `PackVec(0)`/`PackList(0)` empty-collection form, for `[]`/`[,]` and `{}`/`{,}`).
- **`TJump`**: adds its offset to the entry iterator, after checking it is nonzero (a zero offset would be a miscompiled infinite loop, reported as `Status::Unsupported`).
- **`TBranch`**: reads the branch condition with `checkCondition()`, applies `BrFalse` to pick which outcome triggers the jump, jumps if triggered, and pops the condition unless `BrKeepOnBranch`/`BrKeepOnNoBranch` says to keep it for the direction taken - the runtime half of `Rpn::Branch` (see [expression format](rpnexpr.md) for the compile-time half).
- **`TMakeBoolean`**: pops the top value, calls `checkCondition()`, and pushes back a canonical `0`/`1` `Int` - the runtime effect of `Rpn::MakeBoolean`.

At the end of a well-formed expression exactly one value remains on the stack; anything else is a `DBGCHECK`ed internal error (a parser/compiler invariant, not something malformed netlist input can trigger, so it is asserted rather than reported as an ordinary `Status` failure).

## `checkCondition()`

A small protected helper shared by the `TBranch` and `TMakeBoolean` handling: it fetches the top-of-stack `Value` and delegates to `Value::checkCondition()` (see [Values](value.md)).

## `isConstant()`

A separate, cheaper walk over an `Rpn` (no stack machine involved) that answers "can this expression be evaluated once and its value reused, instead of re-evaluated on every access." It inspects only `TIdentifier` entries (must all be named constants, via `ContextStack::isConstant()`) and `TFunctionCall` entries (the called builtin must exist and have `Builtin::pure` set, see [Context](context.md)) - `Jump`/`Branch`/`MakeBoolean` structure doesn't affect the answer, since it never changes which identifiers or functions the expression can reach.

## `appendLocation()`

A thin wrapper around `Status::extend(const Loc&)` (see [Status](status.md)), called at every failure point in `evaluate()` to attach the offending expression's source position to the accumulating error message.
