Predicator (predicator v3.5.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.
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.
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, 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 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.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}
# 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.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]