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
Functions
Assigns value at path_or_expression, returning the rebound context.
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
@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.
@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
@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]}
@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}}
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
@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 asPredicator.evaluate/3's:functionsoption) and:on_unbound(:undefined(default) |:error- seeon_unbound/0; any other value raisesArgumentError)
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}}