Predicator.Context (predicator v3.8.0)

Copy Markdown View Source

A bound evaluation context: data, functions, and the unbound-variable policy.

Build one with new/2, evaluate Predicator.evaluate/3 against it many times, and rebind cheaply with bind/3 between evaluations - the builtin function maps merge once, at construction, not on every evaluate call.

Examples

iex> context = Predicator.Context.new(%{"score" => 85})
iex> Predicator.evaluate("score > 80", context)
{:ok, true}

iex> context = Predicator.Context.new(%{})
iex> context = Predicator.Context.bind(context, "score", 90)
iex> Predicator.evaluate("score", context)
{:ok, 90}

Summary

Types

Policy for a load of an unbound root variable.

t()

A bound evaluation context.

Functions

Assigns value at path_or_expression, returning the rebound context.

Binds name to value in data. O(1): a single Map.put/3, plus normalizing value itself (O(size of value), not O(size of data) - data is already normalized from construction or a prior bind/3).

Answers whether name is bound in context's data, resolved by Predicator.Evaluator.resolve_key/2 - the same lookup the evaluator uses when a load instruction records an unbound read, so the two cannot disagree. Presence, not definedness: a name bound to :undefined is bound.

Builds a context, merging the builtin function maps once.

Types

on_unbound()

@type on_unbound() :: :undefined | :error

Policy for a load of an unbound root variable.

:undefined (the default) pushes the :undefined sentinel and lets three-valued logic absorb it. :error makes the load fail with Predicator.Errors.UndefinedVariableError, halting the run.

Roots only, under either policy: a missing key on a bound map (user.nope), a missing nested path, and an out-of-range index all stay :undefined. This mirrors ECMAScript - a ReferenceError for an undeclared variable, a silent undefined for a missing property - and keeps guards over sparse data usable.

t()

@type t() :: %Predicator.Context{
  data: Predicator.Types.context(),
  functions: %{
    required(binary()) => {Predicator.Evaluator.function_arity(), function()}
  },
  on_unbound: on_unbound()
}

A bound evaluation context.

Functions

assign(context, path_or_expression, value)

@spec assign(
  t(),
  binary() | Predicator.ContextLocation.location_path(),
  Predicator.Types.value()
) ::
  {:ok, t()} | {:error, struct()}

Assigns value at path_or_expression, returning the rebound context.

path_or_expression is either a location expression string (resolved against the context's current data, then written) or an already-resolved Predicator.ContextLocation.location_path/0. Writes through Predicator.ContextLocation.put/3 - the same auto-vivifying write algorithm as Predicator.context_assign/4 and the future store opcode (px-tbv.2).

Examples

iex> context = Predicator.Context.new(%{})
iex> {:ok, context} = Predicator.Context.assign(context, "user.name", "Ada")
iex> context.data
%{"user" => %{"name" => "Ada"}}

iex> context = Predicator.Context.new(%{"items" => [1, 2, 3]})
iex> {:ok, context} = Predicator.Context.assign(context, ["items", 1], "x")
iex> context.data
%{"items" => [1, "x", 3]}

bind(context, name, value)

@spec bind(t(), binary(), Predicator.Types.value()) :: t()

Binds name to value in data. O(1): a single Map.put/3, plus normalizing value itself (O(size of value), not O(size of data) - data is already normalized from construction or a prior bind/3).

value is normalized the same way new/2 normalizes data: atom keys become string keys (string keys win on collision) and nil becomes :undefined, recursing through nested maps and lists; structs pass through unchanged.

functions and on_unbound are carried over unchanged.

Examples

iex> context = Predicator.Context.new(%{"a" => 1})
iex> Predicator.Context.bind(context, "b", 2).data
%{"a" => 1, "b" => 2}

iex> context = Predicator.Context.new(%{})
iex> Predicator.Context.bind(context, "user", %{name: nil}).data
%{"user" => %{"name" => :undefined}}

bound?(context, name)

@spec bound?(t(), binary()) :: boolean()

Answers whether name is bound in context's data, resolved by Predicator.Evaluator.resolve_key/2 - the same lookup the evaluator uses when a load instruction records an unbound read, so the two cannot disagree. Presence, not definedness: a name bound to :undefined is bound.

resolve_key/2 also accepts an atom key, but a Context's data never has one: new/2 and bind/3 normalized them to string keys already. The %{score: 85} example below is bound because of that normalization, not because of an atom lookup here.

Examples

iex> context = Predicator.Context.new(%{"score" => 85})
iex> Predicator.Context.bound?(context, "score")
true
iex> Predicator.Context.bound?(context, "missing")
false

iex> context = Predicator.Context.new(%{score: 85})
iex> Predicator.Context.bound?(context, "score")
true

new(data \\ %{}, opts \\ [])

@spec new(
  Predicator.Types.context(),
  keyword()
) :: t()

Builds a context, merging the builtin function maps once.

Parameters

  • data - the bound-variable map (default %{})
  • opts - :functions (custom functions merged over the builtins, same as Predicator.evaluate/3's :functions option) and :on_unbound (:undefined (default) | :error - see on_unbound/0; any other value raises ArgumentError)

data is normalized deeply before it is stored: atom keys become string keys (a string key wins if both are present at the same level) and nil values become the :undefined sentinel, recursing through nested maps and lists. Date, DateTime, and any other struct pass through unchanged.

Examples

iex> Predicator.Context.new(%{"x" => 1}).data
%{"x" => 1}

iex> Predicator.Context.new(%{user: %{name: nil}}).data
%{"user" => %{"name" => :undefined}}

iex> Predicator.Context.new().on_unbound
:undefined

iex> context = Predicator.Context.new(%{}, on_unbound: :error)
iex> {:error, error} = Predicator.evaluate("missing OR true", context)
iex> {error.variable, error.position}
{"missing", {1, 1}}