# `Predicator.Evaluator`
[🔗](https://github.com/riddler/predicator-ex/blob/v9.4.1/lib/predicator/evaluator.ex#L1)

Stack-based evaluator for predicator instructions.

The evaluator executes a list of instructions using a stack machine approach.
Instructions operate on a stack, with the most recent values at the top (head of list).

The instruction set - every opcode this module accepts, its operands, stack
effect, and error semantics - is specified in
[`docs/isa.md`](../../docs/isa.md), not here. `and` and `or` were retired at
ISA v3: a stored pre-3.7 artifact containing either is refused with a
message naming the removing version and pointing at
`Predicator.Instructions.upgrade/1`, though the ISA table (`docs/isa.md` §4)
keeps their rows.

# `function_arity`

```elixir
@type function_arity() :: non_neg_integer() | [non_neg_integer()]
```

A function's accepted argument count(s): a fixed arity, or a set of arities for optional/variadic-style args

# `t`

```elixir
@type t() :: %Predicator.Evaluator{
  context: Predicator.Types.context(),
  functions: %{
    required(binary()) =&gt;
      {function_arity(), Predicator.Context.function_entry()}
  },
  halted: boolean(),
  host: term(),
  instruction_pointer: non_neg_integer(),
  instructions: Predicator.Types.instruction_list() | tuple(),
  last_value: Predicator.Types.value(),
  loop_budget: non_neg_integer(),
  on_unbound: Predicator.Context.on_unbound(),
  positions: Predicator.Types.position_table() | Predicator.Types.span_table(),
  protected_roots: [binary()],
  segment_positions: Predicator.Types.segment_position_table(),
  size: non_neg_integer() | nil,
  stack: [Predicator.Types.value()],
  unbound_loads: [unbound_load()]
}
```

Internal evaluator state

# `unbound_load`

```elixir
@type unbound_load() ::
  {binary(), Predicator.Types.position() | Predicator.Types.span() | nil}
```

An executed load of a root that was not bound, paired with the source location
of the `["load", _]` instruction that read it.

The location is the raw `positions` table entry: `nil` when the run carried no
table or the index was uncovered, a `t:Predicator.Types.position/0` under point
positions, a `t:Predicator.Types.span/0` under spans. Hand it to
`Predicator.Errors.put_position/2`, which discriminates the three.

# `default_loop_budget`

```elixir
@spec default_loop_budget() :: non_neg_integer()
```

The default number of back edges a single execution may take.

ADR-0013: a program with no `jump_backward` never touches this counter, so
the v5 termination-by-construction property is unaffected. The value is
implementation-local, exactly as `:on_unbound` is - ISA v6 makes only the
*existence* of a bound and the `"loop_budget_exceeded"` reason normative.

# `evaluate`

```elixir
@spec evaluate(
  Predicator.Types.instruction_list(),
  Predicator.Types.context(),
  keyword()
) ::
  Predicator.Types.internal_result()
```

Evaluates a list of instructions with the given context and options.

Returns the top value on the stack when evaluation completes,
or an error if something goes wrong.

## Parameters

- `instructions` - List of instructions to execute
- `context` - Context map with variable bindings (default: `%{}`)
- `opts` - Options keyword list:
  - `:functions`, `:providers`, `:builtins` - resolved into the dispatch
    map the same way `Predicator.Context.new/2` resolves them (see
    `Predicator.Context.resolve_functions/1`): the builtin providers
    (unless `builtins: false`), then `:providers` left to right, then the
    inline `:functions` closure map `%{name => {arity, function}}`, each
    shadowing a same-named entry from an earlier source
  - `:host` - opaque term threaded to every function call's `%Context{}`
    as `context.host` (default `nil`)
  - `:positions` - Side table mapping a 0-based instruction index to the
    `{line, column}` of the AST node that emitted it, as produced by
    `Predicator.Compiler.to_instructions_with_positions/2` and carried in a
    `t:Predicator.Compiled.t/0`'s `positions` field. Runtime errors raised by
    an instruction with a table entry carry it as `:position`. A span table
    from `compiled.positions`, where `compiled` is what
    `Predicator.compile_with_spans/1` returned, works here too: such an
    error carries the span as `:span` and the span's start as `:position`.
  - `:segment_positions` - the per-store segment-position side table from
    `compiled.segment_positions`, as produced by
    `Predicator.Compiler.to_instructions_with_segment_positions/2`. Read only
    by `store`, to blame the exact failing location segment rather than the
    store instruction's own `positions` entry (the lhs root). A run carrying
    no table - or none for the failing store - positions a store failure
    exactly as `:positions` alone would; passing this option changes no
    other opcode's behavior.
  - `:on_unbound` - `:undefined` (default) or `:error`. Under `:error`, a
    `["load", name]` whose `name` is not present in `context` returns
    `{:error, %Predicator.Errors.UndefinedVariableError{}}` instead of
    pushing `:undefined`. Unlike `Predicator.Context.new/2`, this option is
    not validated here: any value other than `:error` behaves as
    `:undefined`.
  - `:loop_budget` - the number of back edges (`jump_backward`, ISA v6) a
    single execution may take, default `10000`, shared
    across every loop in the program. Exhaustion returns
    `{:error, %Predicator.Errors.EvaluationError{reason: "loop_budget_exceeded"}}`.
    Must be a non-negative integer; anything else raises `ArgumentError`.
  - `:protected_roots` - a list of context root names (binaries) a `store`
    may not write, default `[]`. A `store` whose path's root segment is in
    the list returns
    `{:error, %Predicator.Errors.EvaluationError{reason: "protected_root"}}`
    naming the root in `details.root` instead of writing it. Root-segment
    matching only - protection is per-root, not per-path. Must be a list of
    binaries; anything else raises `ArgumentError`.

## Examples

    iex> Predicator.Evaluator.evaluate([["lit", 42]], %{})
    42

    iex> Predicator.Evaluator.evaluate([["load", "limit"]], %{"limit" => 85})
    85

    # With custom functions
    iex> custom_functions = %{"double" => {1, fn [n], _context -> {:ok, n * 2} end}}
    iex> instructions = [["lit", 21], ["call", "double", 1]]
    iex> Predicator.Evaluator.evaluate(instructions, %{}, functions: custom_functions)
    42

# `evaluate!`

```elixir
@spec evaluate!(
  Predicator.Types.instruction_list(),
  Predicator.Types.context(),
  keyword()
) ::
  Predicator.Types.value()
```

Evaluates a list of instructions with the given context and options, raising on errors.

Similar to `evaluate/3` but raises an exception for error results instead
of returning error tuples. Follows the Elixir convention of bang functions.

## Examples

    iex> Predicator.Evaluator.evaluate!([["lit", 42]], %{})
    42

    iex> Predicator.Evaluator.evaluate!([["load", "limit"]], %{"limit" => 85})
    85

    # With custom functions
    iex> custom_functions = %{"double" => {1, fn [n], _context -> {:ok, n * 2} end}}
    iex> instructions = [["lit", 21], ["call", "double", 1]]
    iex> Predicator.Evaluator.evaluate!(instructions, %{}, functions: custom_functions)
    42

# `evaluate_prepared`

```elixir
@spec evaluate_prepared(t()) :: Predicator.Types.internal_result()
```

Runs an already-built evaluator to completion and extracts its result.

Unlike `evaluate/3`, this does no function merging - `evaluator.functions`
is used as given. This is what `evaluate/3` uses internally, and what
`Predicator.Context`-based evaluation uses to skip the per-call merge.

# `last_value`

```elixir
@spec last_value(t()) :: Predicator.Types.value()
```

The value the most recently executed `["pop"]` instruction discarded, or
`:undefined` if the run executed none.

For a program the compiler produced this is **the last expression statement's
value**: `pop` is emitted after every expression statement and after nothing
else, so the last one to run carries the last statement's value. It is defined
over the instruction list rather than over the AST, so it answers for a
hand-built list too.

Two things it deliberately is not:

- **Not the stack top at halt.** A list that leaves a residue on the stack
  without popping it reports `:undefined`; `docs/isa.md` section 2 discards the
  residue.
- **Not written by `jump_if_falsy_or_pop` / `jump_if_true_or_pop`.** Those pop
  as part of a jump, mid-expression, and their operand is not a statement's
  value.

`Predicator.execute_value/2` is the façade over this.

## Examples

    iex> instructions = [["lit", 1], ["pop"], ["lit", 2], ["pop"]]
    iex> {:ok, final} = Predicator.Evaluator.run_state(%Predicator.Evaluator{instructions: instructions})
    iex> Predicator.Evaluator.last_value(final)
    2

    iex> {:ok, final} = Predicator.Evaluator.run_state(%Predicator.Evaluator{instructions: [["lit", 1]]})
    iex> Predicator.Evaluator.last_value(final)
    :undefined

# `loop_budget_from_opts`

```elixir
@spec loop_budget_from_opts(keyword()) :: non_neg_integer()
```

Reads and validates the `:loop_budget` evaluation option.

Raises `ArgumentError` for anything that is not a non-negative integer -
host-API misuse, not a predicate-derived failure (ADR-0004), the same line
`Predicator.Context.new/2` draws for a malformed `:on_unbound`.

# `protected_roots_from_opts`

```elixir
@spec protected_roots_from_opts(keyword()) :: [binary()]
```

Reads and validates the `:protected_roots` evaluation option.

A list of root names (binaries) a `store` may not write. Default `[]` - the
empty list is the "option absent" case and costs one `Enum.member?/2` against
an empty list per store.

Raises `ArgumentError` for anything that is not a list of binaries - host-API
misuse, not a predicate-derived failure (ADR-0004), the same line
`loop_budget_from_opts/1` and `Predicator.Context.new/2` draw.

# `resolve_key`

```elixir
@spec resolve_key(Predicator.Types.context(), binary()) ::
  {:ok, binary() | atom()} | :unbound
```

Resolves `name` to the key it is bound under in `data`, or `:unbound`.

A root variable may be stored under a string key or the equivalent atom key;
this answers which. `Predicator.Context.bound?/2` and the evaluator's own
unbound-load recording both go through here, so they cannot drift apart from
each other.

Note this asks about *presence*, not value: a name bound to `:undefined` (or
to `nil`) resolves to `{:ok, key}`, because it is bound.

> #### Atom keys resolve here but no longer load {: .info}
>
> This is deliberately *wider* than what a `load` instruction reads. Since
> `px-8um.2`, `load` does a plain string-key lookup: `Predicator.Context.new/2`
> and `bind/3` normalize atom keys away at the edge, so no `Context`-routed
> data can still carry one. The atom branch below survives only for a caller
> who bypasses that edge - `Predicator.Evaluator.evaluate/3` or
> `Predicator.evaluator/2` handed a hand-built atom-keyed map.
>
> For that caller the two answers are asymmetric: `%{limit: 85}` *loads* as
> `:undefined` (no string key) while resolving as `{:ok, :limit}` (present),
> so the load is not recorded in `unbound_loads`. Bound-but-undefined is a
> state this API already models, and treating a present atom key as unbound
> bookkeeping would be the worse answer. Nothing reachable through
> `Predicator.evaluate/3` can observe the asymmetry.

## Examples

    iex> Predicator.Evaluator.resolve_key(%{"limit" => 85}, "limit")
    {:ok, "limit"}

    iex> Predicator.Evaluator.resolve_key(%{limit: 85}, "limit")
    {:ok, :limit}

    iex> Predicator.Evaluator.resolve_key(%{"limit" => 85}, "missing")
    :unbound

# `run`

```elixir
@spec run(t()) :: {:ok, t()} | {:error, struct()}
```

Runs the evaluator until it halts or encounters an error.

Returns `{:ok, final_state}` on success or `{:error, reason}` on failure.

# `run_prepared`

```elixir
@spec run_prepared(t()) ::
  {:ok, Predicator.Types.value(), t()} | {:error, struct(), t()}
```

Runs an already-built evaluator to completion, returning its result **and**
its final state - on the error path too.

Same as `evaluate_prepared/1` except that the caller keeps the state the run
produced - `unbound_loads/1` in particular, which `Predicator.evaluate/3`
reads to name the variable behind an `:undefined` result *and* behind a
`TypeMismatchError` that rejected an `:undefined` operand (`px-8um.7`).

# `run_state`

```elixir
@spec run_state(t()) :: {:ok, t()} | {:error, struct(), t()}
```

Runs an already-built evaluator to halt, returning the final state on both
paths.

This is statement mode's entry point (`docs/isa.md` section 2): unlike
`run_prepared/1` it applies no expression-mode result rule, so an empty
stack at halt is a normal halt rather than `empty_stack`. On error the state
returned is the pre-step one, holding every write the statements before the
failing instruction performed - a `["load", _]` instruction never errors,
so the pre-step state holds every unbound load recorded before the failing
instruction too, which is all of them.

# `step`

```elixir
@spec step(t()) :: {:ok, t()} | {:error, term()}
```

Executes a single instruction step.

Returns the updated evaluator state or an error.

# `types_match`
*macro* 

# `unbound_loads`

```elixir
@spec unbound_loads(t()) :: [binary()]
```

The root variables this run loaded and did not find bound, in execution
order, without repeats.

This is what the run *actually read*, not what the program contains: a load
inside a branch `jump_if_falsy_or_pop`/`jump_if_true_or_pop` skipped never
executes and never appears here. `Predicator.evaluate/3` uses it to name the
variable behind an `:undefined` result.

It records executed loads, not provenance - a run that executes two unbound
loads lists both, even if only the second one's `:undefined` reached the
result. Deciding that would require tracking which stack value descends from
which load.

`unbound_loads_with_locations/1` returns the same loads with the source
location of each.

## Examples

    iex> instructions = [["load", "a"], ["load", "b"]]
    iex> {:ok, _result, evaluator} =
    ...>   Predicator.Evaluator.run_prepared(%Predicator.Evaluator{
    ...>     instructions: instructions,
    ...>     context: %{"a" => 1}
    ...>   })
    iex> Predicator.Evaluator.unbound_loads(evaluator)
    ["b"]

# `unbound_loads_with_locations`

```elixir
@spec unbound_loads_with_locations(t()) :: [unbound_load()]
```

The same loads `unbound_loads/1` reports, each paired with the source location
of the `["load", _]` instruction that read it.

The location comes from the run's `positions` table, read at the load itself, so
it names the *variable's* token - not the operator that later rejected its
`:undefined`. It is `nil` when the run carried no table (an instruction-list
caller who passed no `positions:`), a `{line, column}` under point positions, and
a span under a table from `compiled.positions`, where `compiled` is the
`t:Predicator.Compiled.t/0` `Predicator.compile_with_spans/1` returned.
`Predicator.evaluate/3` uses this to position the `UndefinedVariableError` it
builds after the run.

A name loaded more than once appears once, with the location of its first
executed load.

## Examples

    iex> instructions = [["load", "a"], ["load", "b"]]
    iex> {:ok, _result, evaluator} =
    ...>   Predicator.Evaluator.run_prepared(%Predicator.Evaluator{
    ...>     instructions: instructions,
    ...>     context: %{},
    ...>     positions: %{1 => {1, 5}}
    ...>   })
    iex> Predicator.Evaluator.unbound_loads_with_locations(evaluator)
    [{"a", nil}, {"b", {1, 5}}]

---

*Consult [api-reference.md](api-reference.md) for complete listing*
