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]