Rete.DSL.Bindings (Rete v0.1.0)

Copy Markdown View Source

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

binder()

@type binder() :: Rete.IR.Fact.t() | Rete.IR.Coll.t()

A condition that binds variables from a fact.

bound()

@type bound() :: MapSet.t(atom())

The set of variable names bound at a point in the LHS.

Functions

check_guard_vars!(condition, own, bound)

@spec check_guard_vars!(binder(), bound(), bound()) :: :ok

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.

check_test_vars!(test, bound)

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

classify(env, production)

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.

classify_condition(env, condition, bound)

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

classify_elements(env, list, bound)

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

filter_vars(condition)

@spec filter_vars(binder()) :: [atom()]

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 [].

guard_vars(ast)

@spec guard_vars(Macro.t() | nil) :: [atom()]

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.

mark_inert(production)

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

split_guard(guard, local)

@spec split_guard(Macro.t() | nil, bound() | [atom()]) ::
  {Macro.t() | nil, Macro.t() | nil}

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}