Expression construction and code generation: the last phase of the DSL front end.
Internal. The last phase before Rete.IR.escape/1. It constructs the
Rete.IR.Expr descriptor of every executable the IR needs, and emits the quoted
definitions spliced into the ruleset module. Both live here so the naming and hashing
scheme has one implementation.
| kind | arity | signature |
|---|
| :alpha | 1 | (fact) -> bindings_map | nil |
| :join_filter | 2 | (token_bindings, fact_bindings) -> boolean |
| :test | 1 | (bindings_map) -> boolean |
| RHS | 2 | (hash, bindings_map) -> facts |
An alpha matches a fact of any type on purpose, because the alpha index applies
type filtering. That is why it signals no match with nil while a test and a join
filter return false.
A code is <kind>_<type>_bind_<v1>_<v2>_..._expr_<hash>, where the variables are
sorted and the hash is :erlang.phash2/1 of the meta-stripped {args, body} pair.
Two expressions with the same code behave identically, and expr_defs/1 guards every
definition with Module.defines?/2, so two rules of one module sharing a condition
share one function.
The RHS destructures a guaranteed binding in the head and reads an optional one
with Map.get/2. Only variables the body reads are bound, so an ignored one becomes
%{name: _name} and the rule compiles under --warnings-as-errors. The map keys are
untouched. See docs/design/ir.md §5.
Summary
Functions
Builds the alpha Rete.IR.Expr of a condition, (fact) -> bindings_map | nil.
The stable hash of an AST fragment.
Asserts that an expression code always sees the same module attribute values.
The complete quoted body a defrule/defquery expands to.
Joins a code prefix, the sorted variable names and the hash into an expression code.
The quoted definition of a single expression function.
The quoted definitions of every expression function of a production.
The stable hash of an expression, from its argument pattern and its body.
The name of the function generated for an expression code, :"__<code>__".
Builds a join filter, (token_bindings, fact_bindings) -> boolean.
The quoted definition of a query's own function, or nil for a rule.
The quoted definition of the RHS function, (hash, bindings_map) -> facts.
Builds the Rete.IR.Expr of a test over bindings, (bindings_map) -> boolean.
Renders a fact type for use inside an expression code.
Types
Functions
@spec alpha_expr(atom() | module(), Macro.t(), Macro.t(), Macro.t() | nil, bind()) :: Rete.IR.Expr.t()
Builds the alpha Rete.IR.Expr of a condition, (fact) -> bindings_map | nil.
pattern is the pattern as written and is only used to compute the stable
hash, args_ast is the compiled argument pattern from
Rete.DSL.Parser.compile_pattern/2, guard is the alpha part of the per
condition guard (or nil), and bind maps every bound variable to its AST.
Because the hash is taken over {pattern, body}, a condition whose guard was
wholly lifted into a join filter produces exactly the same code as the same
condition written without a guard, and shares its alpha node.
@spec ast_hash(Macro.t()) :: non_neg_integer()
The stable hash of an AST fragment.
Two normalisations run first, and both exist so that the hash is a function of what the code means rather than of how it was typed:
- metadata is stripped, so a rule keeps its hash when it moves down a file;
- discarded variables are canonicalised to
_, so{:order, _x}and{:order, _y}— which compile to byte identical functions, since a_-prefixed name is never a binding — share one expression and therefore one alpha node.
A module attribute hashes as its name, because its value cannot be known
here: @limit expands to a hidden Module.__get_attribute__ call that only runs
once the module body is evaluated, which is after every macro in the body has
expanded. Two conditions over the same pattern therefore share a code whatever
the attribute is currently worth, which is what keeps them sharing an alpha
node in the ordinary case where the value has not changed. The case where it
has changed is caught by check_attr_values!/3 when the body runs.
Asserts that an expression code always sees the same module attribute values.
Called from the ruleset module's body, where attribute values are readable, once per generated expression that mentions an attribute. Raises when a code that was already generated is reached again with a different value, which would otherwise silently reuse the first rule's compiled function.
@spec compile(Rete.IR.Production.t()) :: Macro.t()
The complete quoted body a defrule/defquery expands to.
In order: the query function (queries only), the expression functions, the
escaped production appended to @rule_data, and the RHS function. The escaped
production captures the expression functions by name, so it must come after
their definitions.
The query function comes first so that a @doc written above the defquery
attaches to it — the one definition of the four a caller ever names.
Joins a code prefix, the sorted variable names and the hash into an expression code.
@spec expr_def(Rete.IR.Expr.t()) :: Macro.t()
The quoted definition of a single expression function.
An arity 1 expression matches the fact (or the bindings map) against its
argument pattern; an arity 2 join filter matches both sides at once. The
fallback of a non matching argument is nil for an alpha and false for a
test or a join filter, matching each kind's documented return type.
@spec expr_defs(Rete.IR.Production.t()) :: [Macro.t()]
The quoted definitions of every expression function of a production.
Deduplicated by name within the production and guarded with
Module.defines?/2 across productions, so a condition shared by two rules of
the same module is compiled once.
@spec expr_hash(Macro.t(), Macro.t()) :: non_neg_integer()
The stable hash of an expression, from its argument pattern and its body.
Metadata (and therefore line numbers) is stripped first, so the same source text always hashes the same wherever it is written.
The name of the function generated for an expression code, :"__<code>__".
@spec join_filter_expr(atom() | module(), MapSet.t(atom()) | [atom()], Macro.t()) :: Rete.IR.Expr.t()
Builds a join filter, (token_bindings, fact_bindings) -> boolean.
This is what makes a cross condition guard work. local is the set of
variables the condition's own pattern binds; every variable the guard reads is
destructured from the fact side when it is local and from the token
side otherwise, so a join variable is never bound twice in the same pattern.
The body is wrapped in if ..., do: true, else: false so that the documented
boolean contract holds whatever the user wrote.
@spec query_def(Rete.IR.Production.t()) :: Macro.t() | nil
The quoted definition of a query's own function, or nil for a rule.
defquery summary(...) defines summary/1 and summary/2, so a query is run
by calling it:
MyRuleset.summary(session)
MyRuleset.summary(session, cid: 1)It delegates to Rete.Session.query/3 with {__MODULE__, name}, which is
what makes two rulesets free to use the same query name: the pair is the
identity, and the caller writes the module rather than hoping the bare name is
unique.
@spec rhs_def(Rete.IR.Production.t()) :: Macro.t()
The quoted definition of the RHS function, (hash, bindings_map) -> facts.
Named Rete.IR.rhs_name/1 of the production, so defrule loyalty(...)
defines __rhs_loyalty__/2 and leaves loyalty itself alone.
The bindings are read in two ways, decided by Rete.IR.lhs_bindings/1:
- a guaranteed binding is destructured in the head,
%{cid: cid}, so a token that is missing it raises aFunctionClauseErrorinstead of firing the rule with a hole in it; - an optional binding - one only some branches of a disjunction bind -
is read with
Map.get/2in the body, because the tokens of the other branches genuinely do not carry the key. It isnilthere.
Either way only the variables the body actually reads are bound, so a rule
that ignores a join variable still compiles under --warnings-as-errors.
@spec test_expr(Macro.t(), bind()) :: Rete.IR.Expr.t()
Builds the Rete.IR.Expr of a test over bindings, (bindings_map) -> boolean.
Produced by a rule level guard, defrule r(...) when <guard> do.
Renders a fact type for use inside an expression code.
Module types lose their Elixir. prefix and their dots, so MyApp.Order
becomes MyApp_Order and codes stay readable.