Binding classification and guard splitting.
Internal. This runs between gate normalization and code generation. It walks the
LHS in order, carrying the variables bound so far. For every fact or collection
condition, it computes three things: :join_bind (bound upstream already — 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.
The compiler splits a per-condition guard 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 weakens 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 branch is classified in its own binding context, and so is everything downstream. When the branches classify the tail differently, the tail 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, since 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. The production could then 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 — no ordering can fix that. Calling classify/2 on
an unsorted LHS, as a test may, still raises. 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 its function is handed only the token. A
variable no condition on this path binds is a key that is never in that map. The
generated function would fall through to false, and the production would silently
never fire.
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 bind it. Write it as a per-condition guard instead, inside the branch that
does bind it, 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, and :join_filter filled in on
every Rete.IR.Fact and Rete.IR.Coll. It rebuilds alpha expressions 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, get absorbed into those branches — see the moduledoc.
This is exposed so 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 this on a parsed, not
yet classified condition. After classify_condition/3 runs, the guard left on the
condition is the alpha part. That part reads nothing but the condition's own
variables, by construction, so the result is [].
The variables an AST fragment reads, sorted.
Pinned values (^x) and module attributes (@x) are compile-time constants, and this
excludes them. _-prefixed variables are not excluded: _t in amt > _t really
is a read of _t. 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, instead of 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, meaning local to the collection. An inert variable constrains which facts are gathered. It groups nothing, and it 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. Reading it outside the collection is a compile error. Group by adding
{:holiday, day} instead, or collect everything and use Enum.group_by/2 in the body.
A variable an earlier condition bound is a join key, and it 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 goes to the
alpha, when all its variables are local. Any other conjunct goes to the join filter
instead. 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 otherwise demand a boolean of an
expression that is not the one it was written against.
When nothing has to move, this returns the original guard AST untouched. That way the alpha expression keeps its code, and it stays shared.
iex> Rete.DSL.Bindings.split_guard(nil, [:amt])
{nil, nil}