Predicator (predicator v3.5.0)

View Source

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

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:

  1. Instructions are processed sequentially
  2. Each instruction manipulates a stack
  3. 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

compile(expression)

@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"}

compile!(expression)

@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"]]

context_location(expression, context \\ %{}, opts \\ [])

@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 variable user
  • ["user", "name"] - property access user.name
  • ["items", 0] - array access items[0]
  • ["user", "profile", "settings", "theme"] - nested user.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 resolve
  • context - 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

decompile(ast, opts \\ [])

@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 convert
  • 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"}, {: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)"

evaluate(input, context \\ %{}, opts \\ [])

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 evaluate
  • context - 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

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

evaluate!(input, context \\ %{}, opts \\ [])

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 >", %{})

evaluator(instructions, context \\ %{})

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 execution
  • context - 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}

parse(expression)

@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}}}

run_evaluator(evaluator)

@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]