Expression construction and code generation: the last phase of the DSL front end.
Internal. This is the last phase before Rete.IR.escape/1. It constructs the
Rete.IR.Expr descriptor of every executable the IR needs. It 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. expr_defs/1 guards every
definition with Module.defines?/2, so two rules of one module that share a condition
share one function.
The RHS destructures a guaranteed binding in the head, and reads an optional one
with Map.get/2. It binds only the variables the body reads, so an ignored one becomes
%{name: _name}, and the rule compiles under --warnings-as-errors. The map keys stay
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, 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. bind maps every bound variable
to its AST.
The hash is taken over {pattern, body}. So 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 it shares that condition's alpha node.
@spec ast_hash(Macro.t()) :: non_neg_integer()
The stable hash of an AST fragment.
Two normalisations run first. Both exist so the hash is a function of what the code means, not 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}— byte-identical once compiled, 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, and that call only runs
once the module body is evaluated — after every macro in the body has already
expanded. So two conditions over the same pattern share a code, whatever the attribute
is currently worth. This is what keeps them sharing an alpha node, in the ordinary case
where the value has not changed. check_attr_values!/3 catches the case where the
value has changed, when the body runs.
Asserts that an expression code always sees the same module attribute values.
The ruleset module's body calls this, where attribute values are readable, once per generated expression that mentions an attribute. It raises when a code that was already generated is reached again with a different value. Otherwise, the second rule would 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. That is 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 for 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. The compiler destructures every variable the guard reads
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 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 you run a query by
calling it:
MyRuleset.summary(session)
MyRuleset.summary(session, cid: 1)It delegates to Rete.Session.query/3, with {__MODULE__, name}. This is what lets
two rulesets use the same query name freely — the pair is the identity, and the caller
writes the module, instead of 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 after Rete.IR.rhs_name/1 of the production. So defrule loyalty(...) defines
__rhs_loyalty__/2, and leaves loyalty itself alone.
Rete.IR.lhs_bindings/1 decides how the RHS reads the bindings, in two ways:
- a guaranteed binding is destructured in the head,
%{cid: cid}. A token missing it raises aFunctionClauseError, instead 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 instead. The tokens of the other branches genuinely do not carry the key, so it isnilthere.
Either way, only the variables the body actually reads get 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.