Predicator (predicator v2.2.0)
View SourceA 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
Currently supported instructions:
["lit", value]
- Push a literal value onto the stack["load", variable_name]
- Load a variable from context onto the stack["compare", operator]
- Compare top two stack values (GT, LT, EQ, GTE, LTE, NE)
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"}
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.
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.
Parses an expression string into an Abstract Syntax Tree.
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, 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 decompile( Predicator.Parser.ast(), 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 convertopts
- 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"}, {:literal, 85}}
iex> Predicator.decompile(ast)
"score > 85"
iex> ast = {:literal, 42}
iex> Predicator.decompile(ast)
"42"
iex> ast = {:comparison, :eq, {:identifier, "active"}, {:literal, true}}
iex> Predicator.decompile(ast, parentheses: :explicit, spacing: :verbose)
"(active = true)"
@spec evaluate( binary() | Predicator.Types.instruction_list(), Predicator.Types.context(), 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 or instruction list to evaluatecontext
- Optional context map with variable bindings (default:%{}
)opts
- Optional keyword list of options::functions
- Map of custom functions to make available during evaluation
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}
# Error handling
iex> {:error, error} = Predicator.evaluate("score + 'hello'", %{"score" => 5})
iex> error.message
"Arithmetic add requires integers, got 5 (integer) and \"hello\" (string)"
iex> error.expected
:integer
@spec evaluate!( binary() | Predicator.Types.instruction_list(), 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
# 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 parse(binary()) :: {:ok, Predicator.Parser.ast()} | {:error, binary(), pos_integer(), pos_integer()}
Parses an expression string into an Abstract Syntax Tree.
Examples
iex> Predicator.parse("score > 85")
{:ok, {:comparison, :gt, {:identifier, "score"}, {:literal, 85}}}
@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]