Binding classification and guard splitting.
Internal. Runs between gate normalization and code generation. It walks the LHS
in order, carrying the variables bound so far, and for every fact or collection
condition computes :join_bind (already bound upstream, so the hash join keys),
:new_bind (introduced here, which for a collection decides the empty-collection
semantics) and :join_filter (the part of the guard a single fact cannot decide).
join_bind ++ new_bind == bind always holds.
A per-condition guard is split conjunct by conjunct over the top level and/&&
chain, so {:order, id, amt} when amt > 0 and amt > limit puts amt > 0 in the alpha
and amt > limit in the join filter. A guard that cannot be decomposed goes to the join
filter whole. Each half is rejoined with the operators it was written with, and
and is weakened to && once a conjunct has been lifted out, because and is strict
in its left operand.
Every branch of a disjunction is a distinct path through the beta graph, so each is classified in its own binding context, and so is everything downstream. When the branches classify the tail differently it is absorbed into them, bounded at 1024 LHS elements.
Raises at compile time for a guard variable no condition binds on a path, a collection
guard reading its own collection binding, and a right hand side reading a
collection-local variable. See docs/design/ir.md §7.
Summary
Types
A condition that binds variables from a fact.
The set of variable names bound at a point in the LHS.
Functions
Raises unless every variable a condition's guard reads is available to it.
Raises unless every variable a rule level guard reads is bound on its path.
Classifies every condition of a production and splits its guards.
Classifies a single fact or collection condition against the bound variables.
Classifies a list of LHS elements against the variables bound before them.
The variables a condition's guard needs that its own fact cannot supply.
The variables an AST fragment reads, sorted.
Marks the variables that are local to a collection.
Splits a guard into {alpha_guard, join_guard}.
Types
@type binder() :: Rete.IR.Fact.t() | Rete.IR.Coll.t()
A condition that binds variables from a fact.
The set of variable names bound at a point in the LHS.
Functions
Raises unless every variable a condition's guard reads is available to it.
A guard may read the variables its own pattern binds (own, which includes
the fact binding, because the alpha's argument is the fact) and the variables
bound by an earlier condition (bound). Anything else would compile into a
join filter that reads the token side for a variable that is never there, and
the production could never fire.
A forward reference is no longer one of those cases. Rete.Compiler.Sort
reorders the LHS before this phase runs, so a condition whose guard reads a
variable another condition binds has already been moved after it. Reaching
here means no condition on this path binds the variable at all, which no
ordering can fix. Calling classify/2 on an unsorted LHS - as a test may -
still raises, and that is the same defect seen one phase early.
@spec check_test_vars!(Rete.IR.Test.t(), bound()) :: :ok
Raises unless every variable a rule level guard reads is bound on its path.
A Rete.IR.Test has no fact of its own, so the only thing its function is
handed is the token: a variable no condition on this path binds is a key that
is never in that map, the generated function falls through to false, and the
production silently never fires.
The check is path exact. After a disjunction whose branches bind different
variables, classify_elements/3 has absorbed everything downstream into the
branches, so the test is checked once per branch against exactly what that
branch binds. A guard over a variable only some branches bind is therefore an
error on the branches that do not - write it as a per condition guard inside
the branch that does, where it can actually be evaluated.
@spec classify(Rete.DSL.Parser.env(), Rete.IR.Production.t()) :: Rete.IR.Production.t()
Classifies every condition of a production and splits its guards.
Returns the production with :join_bind, :new_bind, :join_filter filled
in on every Rete.IR.Fact and Rete.IR.Coll, and with alpha expressions
rebuilt wherever a guard was partly or wholly lifted into a join filter.
env is the Macro.Env of the defrule/defquery call; it is needed to
re-expand struct aliases when an alpha is rebuilt.
@spec classify_condition(Rete.DSL.Parser.env(), binder(), bound()) :: binder()
Classifies a single fact or collection condition against the bound variables.
Splits the condition's guard, rebuilding the alpha and building the join filter when part or all of the guard has to move to the beta node.
@spec classify_elements(Rete.DSL.Parser.env(), Rete.IR.lhs(), bound()) :: {Rete.IR.lhs(), bound()}
Classifies a list of LHS elements against the variables bound before them.
Returns {classified_elements, bound_after}. The returned list is not
necessarily as long as the one given: the elements that follow a disjunction
whose branches classify them differently are absorbed into those branches, see
the moduledoc.
Exposed so that a caller can classify a fragment, for instance a branch of a disjunction.
The variables a condition's guard needs that its own fact cannot supply.
These are exactly the variables that force a join filter. Call it on a parsed,
not yet classified condition; after classify_condition/3 the guard left on
the condition is the alpha part, which by construction reads nothing but the
condition's own variables, so the result is [].
The variables an AST fragment reads, sorted.
Pinned values (^x) and module attributes (@x) are compile time constants
and excluded. _-prefixed variables are not: _t in amt > _t really is
a read of _t, and treating it as local would inline it into the alpha, where
it is not in scope. Only the anonymous _ is skipped.
@spec mark_inert(Rete.IR.Production.t()) :: Rete.IR.Production.t()
Marks the variables that are local to a collection.
Elixir fuses binding with constraining, so os = [{:order, cid, amt} when amt > lim]
reads as introducing amt. A collection that introduces a variable groups by it, which
would collect one singleton group per distinct amount rather than every order over the
limit.
The rule: a collection's pattern variable participates only if another condition also matches on it. Otherwise it is inert, which means local to the collection. An inert variable constrains which facts are gathered, groups nothing and binds nothing downstream.
Only another condition's pattern counts, never a guard and never the right hand
side. So os = [{:order, cid, day, _amt}] with day read only in the body makes day
inert, and reading it outside the collection is a compile error. Group by adding
{:holiday, day}, or collect everything and use Enum.group_by/2 in the body.
A variable an earlier condition bound is a join key and is never inert. See
docs/design/ir.md §2.
Splits a guard into {alpha_guard, join_guard}.
local is the set (or list) of variables the condition's own pattern binds,
including its fact binding. A conjunct of the top level and/&& chain whose
variables are all local goes to the alpha, any other conjunct goes to the join
filter. Either half may be nil.
Each half is rejoined with the operators the guard was written with, so an
all-&& chain stays an all-&& chain and a guard over a truthy value keeps
working after the split. A conjunct that has lost a predecessor is rejoined
with && even where the source said and: the strict operator would demand a
boolean of an expression that is not the one it was written against.
When nothing has to move, the original guard AST is returned untouched so that the alpha expression keeps its code and stays shared.
iex> Rete.DSL.Bindings.split_guard(nil, [:amt])
{nil, nil}