A secure, non-evaluative condition engine for processing end-user boolean predicates.
Predicator transforms string conditions into executable instructions that can be safely evaluated without direct code execution. It uses a stack-based virtual machine to process instructions and supports flexible context-based condition checking.
Basic Usage
The simplest way to use Predicator is with the evaluate/2 function:
iex> instructions = [["lit", 42]]
iex> Predicator.evaluate(instructions)
{:ok, 42}
iex> instructions = [["load", "score"]]
iex> context = %{"score" => 85}
iex> Predicator.evaluate(instructions, context)
{:ok, 85}Instruction Format
Instructions are lists where:
- First element is the operation name (string)
- Remaining elements are operation arguments
The full opcode set is specified in docs/isa.md.
Context
The context is a map containing variable bindings. Both string and atom keys are supported for flexibility:
%{"score" => 85, "name" => "Alice"}
%{score: 85, name: "Alice"}A bare map is merged with the builtin functions on every evaluate/3 call -
fine for one-off evaluation, wasteful when the same bindings are evaluated
against repeatedly. Predicator.Context.new/2 builds that merge once into a
%Predicator.Context{}, which evaluate/3 also accepts directly:
iex> context = Predicator.Context.new(%{"score" => 85})
iex> Predicator.evaluate("score > 80", context)
{:ok, true}
iex> context = Predicator.Context.bind(context, "score", 90)
iex> Predicator.evaluate("score > 80", context)
{:ok, true}A context also carries the unbound-variable policy. By default a load of an
unbound root pushes the :undefined sentinel, which three-valued logic can
absorb into a defined result; under on_unbound: :error the load fails
instead, naming the variable:
iex> context = Predicator.Context.new(%{}, on_unbound: :error)
iex> {:error, error} = Predicator.evaluate("missing OR true", context)
iex> error.variable
"missing"The same option works on a bare map, which evaluate/3 routes through
Predicator.Context.new/2:
iex> Predicator.evaluate("missing OR true", %{})
{:ok, true}
iex> {:error, error} = Predicator.evaluate("missing OR true", %{}, on_unbound: :error)
iex> error.variable
"missing"Architecture
Predicator uses a stack-based evaluation model:
- Instructions are processed sequentially
- Each instruction manipulates a stack
- The final result is the top value on the stack when execution completes
Summary
Functions
Compiles a string expression to instruction list.
Compiles a string expression to instruction list, raising on errors.
Compiles a statement-sequence string to an instruction list.
Compiles a statement-sequence string to a Predicator.Compiled.t/0 - the
instruction list plus a source-position side table, as one value.
Compiles a string expression to a Predicator.Compiled.t/0 - the
instruction list plus a source-position side table, as one value.
Compiles a string expression to a Predicator.Compiled.t/0 - the
instruction list plus a source-span side table, as one value.
Assigns value at the location expression names within context.
Resolves a location path for assignment operations in SCXML datamodel expressions.
Converts an AST back to a string representation.
Evaluates a predicate expression or instruction list.
Evaluates a predicate expression or instruction list, raising on errors.
Creates a new evaluator state for low-level instruction processing.
Runs a statement program - a source string, an instruction list, or a
Predicator.Compiled.t/0 - and returns the resulting context.
Runs a statement program - a source string, an instruction list, or a
Predicator.Compiled.t/0 - and returns the resulting context plus the
value the program's last expression statement produced.
Returns the ISA version this build emits and can run.
Parses an expression string into an Abstract Syntax Tree.
Parses a statement-sequence string into a Predicator.Parser.program/0.
Runs an evaluator until completion.
Functions
@spec compile(binary()) :: {:ok, Predicator.Types.instruction_list()} | {:error, binary()}
Compiles a string expression to instruction list.
This function allows you to pre-compile expressions for maximum performance when evaluating the same expression multiple times with different contexts.
Parameters
expression- String expression to compile
Returns
{:ok, instructions}- Successfully compiled instructions{:error, message}- Parse error with details
Examples
iex> {:ok, instructions} = Predicator.compile("score > 85")
iex> instructions
[["load", "score"], ["lit", 85], ["compare", "GT"]]
iex> Predicator.compile("score >")
{:error, "Expected number, string, boolean, date, datetime, identifier, function call, list, object, or '(' but found end of input at line 1, column 8"}
@spec compile!(binary()) :: Predicator.Types.instruction_list()
Compiles a string expression to instruction list, raising on errors.
Similar to compile/1 but raises an exception for parse errors.
Examples
iex> Predicator.compile!("score > 85")
[["load", "score"], ["lit", 85], ["compare", "GT"]]
@spec compile_program(binary()) :: {:ok, Predicator.Types.instruction_list()} | {:error, binary()}
Compiles a statement-sequence string to an instruction list.
The program-shaped echo of compile/1: parses with parse_program/2
instead of parse/2, then compiles the resulting Predicator.Parser.program/0
with Compiler.to_instructions/2, which already accepts one. Returns the
same {:error, binary()} shape compile/1 does - not parse_program/2's
raw 4-tuple.
Examples
iex> {:ok, instructions} = Predicator.compile_program("x = 1; x + 1")
iex> instructions
[["lit", "x"], ["lit", 1], ["store", 1], ["load", "x"], ["lit", 1], ["add"], ["pop"]]
iex> Predicator.compile_program("x =")
{:error, "Expected number, string, boolean, date, datetime, identifier, function call, list, object, or '(' but found end of input at line 1, column 4"}
@spec compile_program_with_positions(binary()) :: {:ok, Predicator.Compiled.t()} | {:error, binary()}
Compiles a statement-sequence string to a Predicator.Compiled.t/0 - the
instruction list plus a source-position side table, as one value.
The program-shaped echo of compile_with_positions/1. Pass the struct
straight to execute/3, which threads the table itself.
Examples
iex> {:ok, compiled} = Predicator.compile_program_with_positions("x = 1")
iex> compiled.instructions
[["lit", "x"], ["lit", 1], ["store", 1]]
@spec compile_with_positions(binary()) :: {:ok, Predicator.Compiled.t()} | {:error, binary()}
Compiles a string expression to a Predicator.Compiled.t/0 - the
instruction list plus a source-position side table, as one value.
compiled.instructions is identical to compile/1's output;
compiled.positions maps each instruction's 0-based index to the
{line, column} of the AST node that emitted it. Pass the struct straight
to evaluate/3, which threads the table itself.
Store compiled.instructions, not the struct - see Predicator.Compiled.
Examples
iex> {:ok, compiled} = Predicator.compile_with_positions("score > 85")
iex> compiled.instructions
[["load", "score"], ["lit", 85], ["compare", "GT"]]
iex> compiled.positions
%{0 => {1, 1}, 1 => {1, 9}, 2 => {1, 7}}
@spec compile_with_spans(binary()) :: {:ok, Predicator.Compiled.t()} | {:error, binary()}
Compiles a string expression to a Predicator.Compiled.t/0 - the
instruction list plus a source-span side table, as one value.
compiled.instructions is identical to compile/1's output;
compiled.positions maps each instruction's 0-based index to the
Predicator.Types.span/0 of the AST node that emitted it. Pass the struct
straight to evaluate/3, which threads the table itself.
Store compiled.instructions, not the struct - see Predicator.Compiled.
Examples
iex> {:ok, compiled} = Predicator.compile_with_spans("score > 85")
iex> compiled.instructions
[["load", "score"], ["lit", 85], ["compare", "GT"]]
iex> compiled.positions
%{0 => {{1, 1}, {1, 6}}, 1 => {{1, 9}, {1, 11}}, 2 => {{1, 1}, {1, 11}}}
@spec context_assign(Predicator.Types.context(), binary(), term(), keyword()) :: {:ok, Predicator.Types.context()} | {:error, struct()}
Assigns value at the location expression names within context.
Resolves the location expression the same way context_location/3 does, then
writes through Predicator.ContextLocation.put/3, creating any missing
intermediate maps and lists. See Predicator.ContextLocation.put/3 for the
full auto-vivification and collision rules.
The expression is resolved against the pre-assignment context, which matches
SCXML: a variable bracket key such as items[index] reads index as it stands
before the write.
Argument order
context comes first because this function transforms a context and
returns a new one, which makes it pipeline-friendly, whereas
context_location/3 merely inspects one.
Parameters
context- The context map to write intoexpression- The location expression naming where to writevalue- The value to writeopts- Reserved for future options
Returns
{:ok, context}- The updated context{:error, %Predicator.Errors.LocationError{}}- The expression is not an assignable location, or the write collided with existing data{:error, %Predicator.Errors.ParseError{}}- The expression did not parse
Examples
iex> Predicator.context_assign(%{}, "user.profile.name", "Ada")
{:ok, %{"user" => %{"profile" => %{"name" => "Ada"}}}}
iex> Predicator.context_assign(%{"items" => [1, 2, 3]}, "items[1]", "x")
{:ok, %{"items" => [1, "x", 3]}}
iex> {:error, error} = Predicator.context_assign(%{}, "len(items)", 1)
iex> error.type
:not_assignable
@spec context_location(binary(), Predicator.Types.context(), keyword()) :: {:ok, Predicator.ContextLocation.location_path()} | {:error, struct()}
Resolves a location path for assignment operations in SCXML datamodel expressions.
Takes an expression string and returns a location path that can be used for assignment operations. Validates that the expression represents an assignable location (l-value) rather than a computed value.
This function is specifically designed for SCXML <assign location="..."> operations
where the location attribute must specify where to assign a value in the datamodel.
Location Path Format
Location paths are returned as lists of keys/indices that represent the path to a specific location in the context data structure:
["user"]- top-level variableuser["user", "name"]- property accessuser.name["items", 0]- array accessitems[0]["user", "profile", "settings", "theme"]- nesteduser.profile.settings.theme
Assignable vs Non-Assignable
Assignable (valid locations):
- Simple identifiers:
user - Property access:
user.name,obj.prop - Bracket access:
items[0],obj["key"] - Mixed notation:
user.items[0].name
Non-Assignable (invalid locations):
- Literals:
42,"string",true - Function calls:
len(items),upper(name) - Arithmetic expressions:
user.age + 1 - Any computed values
Parameters
expression- The location expression string to resolvecontext- The evaluation context (used for resolving variable keys)opts- Options (currently unused, reserved for future extensions)
Examples
# Simple identifier
iex> Predicator.context_location("user", %{"user" => %{"name" => "John"}})
{:ok, ["user"]}
# Property access
iex> Predicator.context_location("user.name", %{"user" => %{"name" => "John"}})
{:ok, ["user", "name"]}
# Bracket access with literal key
iex> Predicator.context_location("items[0]", %{"items" => [1, 2, 3]})
{:ok, ["items", 0]}
# Bracket access with string key
iex> Predicator.context_location("user['profile']", %{"user" => %{"profile" => %{}}})
{:ok, ["user", "profile"]}
# Mixed notation
iex> Predicator.context_location("data.users[0]['name']", %{"data" => %{"users" => [%{"name" => "Alice"}]}})
{:ok, ["data", "users", 0, "name"]}
# Variable as bracket key
iex> Predicator.context_location("items[index]", %{"items" => [1, 2, 3], "index" => 1})
{:ok, ["items", 1]}
# Error: cannot assign to literal
iex> {:error, %Predicator.Errors.LocationError{type: :not_assignable}} =
...> Predicator.context_location("42", %{})
# Error: cannot assign to function call
iex> {:error, %Predicator.Errors.LocationError{type: :not_assignable}} =
...> Predicator.context_location("len(items)", %{"items" => [1, 2, 3]})
# Error: cannot assign to computed expression
iex> {:error, %Predicator.Errors.LocationError{type: :not_assignable}} =
...> Predicator.context_location("user.age + 1", %{"user" => %{"age" => 30}})SCXML Usage Example
# In an SCXML state machine
location_expr = "user.profile.settings['theme']"
case Predicator.context_location(location_expr, datamodel_context) do
{:ok, path} ->
# Use path for assignment: ["user", "profile", "settings", "theme"]
update_datamodel_at_path(datamodel, path, new_value)
{:error, %Predicator.Errors.LocationError{} = error} ->
# Handle invalid assignment target
{:error, "Invalid assignment location: #{error.message}"}
end
@spec decompile( Predicator.Parser.visitable(), keyword() ) :: binary()
Converts an AST back to a string representation.
This function takes an Abstract Syntax Tree and generates a readable string representation. This is useful for debugging, displaying expressions to users, and documentation purposes.
Parameters
ast- The Abstract Syntax Tree to convert - a bare expression, or aPredicator.Parser.program/0, which renders as its statements joined by"; "opts- Optional formatting options::parentheses-:minimal(default) |:explicit|:none:spacing-:normal(default) |:compact|:verbose
Returns
String representation of the AST.
Examples
iex> ast = {:comparison, :gt, {:identifier, "score", nil}, {:literal, 85, nil}, nil}
iex> Predicator.decompile(ast)
"score > 85"
iex> ast = {:literal, 42, nil}
iex> Predicator.decompile(ast)
"42"
iex> ast = {:comparison, :eq, {:identifier, "active", nil}, {:literal, true, nil}, nil}
iex> Predicator.decompile(ast, parentheses: :explicit, spacing: :verbose)
"(active == true)"
iex> {:ok, ast} = Predicator.parse_program("if a { x = 1 } else { x = 2 }")
iex> Predicator.decompile(ast)
"if a { x = 1 } else { x = 2 }"
@spec evaluate( binary() | Predicator.Types.instruction_list() | Predicator.Compiled.t(), Predicator.Types.context() | Predicator.Context.t(), keyword() ) :: {:ok, Predicator.Types.value()} | {:error, struct()}
Evaluates a predicate expression or instruction list.
This is the main entry point for Predicator evaluation. It accepts either:
- A string expression (e.g., "score > 85") which gets compiled automatically
- A pre-compiled instruction list for maximum performance
Parameters
input- String expression, instruction list, or aPredicator.Compiled.t/0fromcompile_with_positions/1orcompile_with_spans/1, whose table is threaded automaticallycontext- Optional context map with variable bindings (default:%{})opts- Optional keyword list of options::functions- Map of custom functions to make available during evaluation,%{name => {arity, fun}}, called as(args, context):providers- a list ofPredicator.FunctionProvidermodules, resolved into the dispatch map alongside the builtins and:functions:builtins-falsedrops the four default builtin functions (defaulttrue):host- an opaque term threaded to every function call's context ascontext.host(defaultnil) - seePredicator.Context.put_host/2:positions- the table fromcompiled.positions, for a caller passing a bare instruction list, used to populate:position(and:span) on runtime errors. String input threads its own table automatically; an instruction-list caller who omits this seesposition: nil. Passing it alongside a%Compiled{}raisesArgumentError.evaluate/3compiles nostore, so the companion:segment_positionstable (seeexecute/3) is always empty here and has no observable effect.:spans- whentrue, string input compiles with spans instead of point positions, so runtime errors carry:spanand:positionnames the span's start. Ignored for instruction-list input, which has no source; such a caller passescompile_with_spans/1's struct, orpositions:fromcompiled.positions.:loop_budget- the number of back edges (jump_backward, ISA v6) a single execution may take, defaultPredicator.Evaluator.default_loop_budget/0(10_000), 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 raisesArgumentError.
Returns
{:ok, result}on successful evaluation{:error, error_struct}if parsing or execution fails
Error Types
Predicator.Errors.TypeMismatchError- Type mismatch in operationPredicator.Errors.UndefinedVariableError- Variable not found in contextPredicator.Errors.EvaluationError- General evaluation error (division by zero, etc.)Predicator.Errors.ParseError- Expression parsing error
Examples
# Simple expressions
iex> Predicator.evaluate("true")
{:ok, true}
iex> Predicator.evaluate("2 + 3")
{:ok, 5}
# With context
iex> Predicator.evaluate("score > 85", %{"score" => 90})
{:ok, true}
# With custom functions
iex> custom_functions = %{"double" => {1, fn [n], _context -> {:ok, n * 2} end}}
iex> Predicator.evaluate("double(21)", %{}, functions: custom_functions)
{:ok, 42}
# Pre-compiled instruction lists
iex> Predicator.evaluate([["lit", 42]])
{:ok, 42}
# Type coercion with + operator (string concatenation)
iex> Predicator.evaluate("score + 'hello'", %{"score" => 5})
{:ok, "5hello"}
# Error handling for incompatible types
iex> {:error, error} = Predicator.evaluate("score * true", %{"score" => 5})
iex> String.contains?(error.message, "multiply requires")
true
@spec evaluate!( binary() | Predicator.Types.instruction_list() | Predicator.Compiled.t(), Predicator.Types.context(), keyword() ) :: Predicator.Types.value()
Evaluates a predicate expression or instruction list, 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.evaluate!("score > 85", %{"score" => 90})
true
iex> Predicator.evaluate!([["lit", 42]])
42
# With custom functions
iex> custom_functions = %{"double" => {1, fn [n], _context -> {:ok, n * 2} end}}
iex> Predicator.evaluate!("double(21)", %{}, functions: custom_functions)
42
# A pre-compiled %Predicator.Compiled{}
iex> {:ok, compiled} = Predicator.compile_with_positions("score > 85")
iex> Predicator.evaluate!(compiled, %{"score" => 90})
true
# This would raise an exception:
# Predicator.evaluate!("score >", %{})
@spec evaluator(Predicator.Types.instruction_list(), Predicator.Types.context()) :: Predicator.Evaluator.t()
Creates a new evaluator state for low-level instruction processing.
This function is useful when you need fine-grained control over the evaluation process or want to inspect the evaluator state.
Parameters
instructions- List of instructions to prepare for executioncontext- Optional context map with variable bindings (default:%{})
Returns
An %Predicator.Evaluator{} struct ready for execution.
Examples
iex> evaluator = Predicator.evaluator([["lit", 42]])
iex> evaluator.instructions
[["lit", 42]]
iex> evaluator = Predicator.evaluator([["load", "x"]], %{"x" => 10})
iex> evaluator.context
%{"x" => 10}
@spec execute( binary() | Predicator.Types.instruction_list() | Predicator.Compiled.t(), Predicator.Types.context() | Predicator.Context.t(), keyword() ) :: {:ok, Predicator.Context.t()} | {:error, struct(), Predicator.Context.t()}
Runs a statement program - a source string, an instruction list, or a
Predicator.Compiled.t/0 - and returns the resulting context.
This is statement mode's entry point (docs/isa.md section 2), the sibling
of evaluate/3 for the {:program, ...} / {:assignment, ...} grammar
parse_program/2 produces. Where evaluate/3 returns the value an
expression reduces to, execute/3 returns the context a program's
writes produced - a program halts with an empty stack by design, so there
is no expression result to return.
Parameters
Same shape as evaluate/3:
program_or_source- a source string (parsed withparse_program/2, notparse/2), a pre-compiled instruction list, or aPredicator.Compiled.t/0fromcompile_program_with_positions/1context- a bare map or aPredicator.Context.t/0(default%{})opts- same optionsevaluate/3accepts (:functions,:providers,:builtins,:host,:positions,:segment_positions,:on_unbound,:loop_budget);:positionsor:segment_positionsalongside a%Compiled{}raisesArgumentError, same asevaluate/3. Unlikeevaluate/3, a program can compile astore, so:segment_positions- the table fromcompiled.segment_positions- can change a store failure's reported position here; seePredicator.Evaluator.evaluate/3's option list.
Returns
{:ok, context}- every statement ran;context.dataholds every write{:error, error_struct, context}- execution stopped at the failing statement.contextis the context as of the last successfully completed statement - every earlier write survives, the failing statement's write does not happen, and no later statement runs. Handing back this partial context costs nothing (contexts are immutable) and commit-or-discard is left to the caller: a caller wanting all-or-nothing drops the third element and keeps the context it already had.case Predicator.execute(program, ctx) do {:ok, ctx} -> ctx {:error, _error, _partial} -> ctx # all-or-nothing is a caller policy end
A caller who also wants the program's last expression statement's value
calls execute_value/3 instead.
Examples
iex> {:ok, ctx} = Predicator.execute("x = 1; y = x + 2", %{})
iex> ctx.data
%{"x" => 1, "y" => 3}
iex> {:error, %Predicator.Errors.EvaluationError{reason: "not_a_container"}, ctx} =
...> Predicator.execute("a = 1; a.b = 2; c = 3", %{})
iex> ctx.data
%{"a" => 1}
@spec execute_value( binary() | Predicator.Types.instruction_list() | Predicator.Compiled.t(), Predicator.Types.context() | Predicator.Context.t(), keyword() ) :: {:ok, Predicator.Types.value(), Predicator.Context.t()} | {:error, struct(), Predicator.Context.t()}
Runs a statement program - a source string, an instruction list, or a
Predicator.Compiled.t/0 - and returns the resulting context plus the
value the program's last expression statement produced.
This is execute/3 plus the last expression statement's value; call
execute/3 instead when the value is not wanted.
The value is the value of the program's last expression statement:
:undefined when the program has none (a program of assignments only), and
:undefined for a hand-built instruction list that pops nothing. See
Predicator.Evaluator.last_value/1 for the exact definition, which this
function projects.
Parameters
Same as execute/3: program_or_source, context (default %{}), and
opts (default []), including :positions or :segment_positions
alongside a %Compiled{} raising ArgumentError.
Returns
{:ok, value, context}- every statement ran;valueis the last expression statement's value (or:undefined),context.dataholds every write{:error, error_struct, context}- identical toexecute/3's error arm: no value is reported for a run that stopped early. A caller who wants the partial run's last value hasEvaluator.run_state/1andEvaluator.last_value/1directly.
This is a host-API convenience, not an ISA guarantee - see
docs/isa.md section 2's host-convenience paragraph. A sibling
implementation need not offer it.
Examples
iex> {:ok, value, ctx} = Predicator.execute_value("x = 2; x * 10", %{})
iex> value
20
iex> ctx.data
%{"x" => 2}
iex> {:ok, value, _ctx} = Predicator.execute_value("x = 1", %{})
iex> value
:undefined
iex> {:error, %Predicator.Errors.EvaluationError{reason: "not_a_container"}, ctx} =
...> Predicator.execute_value("a = 1; a.b = 2; c = 3", %{})
iex> ctx.data
%{"a" => 1}
@spec isa_version() :: pos_integer()
Returns the ISA version this build emits and can run.
ISA versions are integers, independent of this library's semantic version
(ADR-0003). Compare it against Predicator.Instructions.required_isa/1
before running a stored instruction list, so a version mismatch is caught
up front instead of failing partway through evaluation. See
docs/isa.md for the full versioning scheme.
Examples
iex> Predicator.isa_version()
6
@spec parse( binary(), keyword() ) :: {:ok, Predicator.Parser.ast()} | {:error, binary(), pos_integer(), pos_integer()}
Parses an expression string into an Abstract Syntax Tree.
Every node carries a trailing {line, column} source position. Pass
spans: true for a Predicator.Types.span/0 in that slot instead - the
source text the node covers, which is what a diagnostic underlines.
Examples
iex> Predicator.parse("score > 85")
{:ok, {:comparison, :gt, {:identifier, "score", {1, 1}}, {:literal, 85, {1, 9}}, {1, 7}}}
iex> Predicator.parse("score > 85", spans: true)
{:ok, {:comparison, :gt, {:identifier, "score", {{1, 1}, {1, 6}}}, {:literal, 85, {{1, 9}, {1, 11}}}, {{1, 1}, {1, 11}}}}
@spec parse_program( binary(), keyword() ) :: Predicator.Parser.program_result()
Parses a statement-sequence string into a Predicator.Parser.program/0.
Implements program := statement (";" statement)* [";"], where a statement
is either an assignment (location "=" expression) or an ordinary
expression. This is a separate entry point from parse/2, not an option on
it: parse/2 never returns a program, and parse_program/2 always returns
one, even for a single statement.
Examples
iex> Predicator.parse_program("a = 1; b = a + 1")
{:ok,
{:program,
[
{:assignment, {:identifier, "a", {1, 1}}, {:literal, 1, {1, 5}}, {1, 3}},
{:assignment, {:identifier, "b", {1, 8}},
{:arithmetic, :add, {:identifier, "a", {1, 12}}, {:literal, 1, {1, 16}}, {1, 14}},
{1, 10}}
], {1, 1}}}
iex> Predicator.parse_program("42 = 1")
{:error, "Left side of '=' must be an assignable location - an identifier, a property access, or a bracket access.", 1, 4}
@spec run_evaluator(Predicator.Evaluator.t()) :: {:ok, Predicator.Evaluator.t()} | {:error, term()}
Runs an evaluator until completion.
This provides direct access to the low-level evaluator API for cases
where you need more control than the execute/2 function provides.
Parameters
evaluator- An%Predicator.Evaluator{}struct
Returns
{:ok, final_evaluator_state}on success{:error, reason}on failure
Examples
iex> evaluator = Predicator.evaluator([["lit", 42]])
iex> {:ok, final_state} = Predicator.run_evaluator(evaluator)
iex> final_state.stack
[42]