Turns the quoted arguments of Rete.Ruleset.defrule/2 and
Rete.Ruleset.defquery/2 into Rete.IR structs.
Internal. This is the first phase of the DSL front end. It records each LHS
element's type, bindings, and guard. It builds the alpha and test Rete.IR.Expr
descriptors. It keeps the raw pattern and guard AST in :__ast__, for the later phases.
It deliberately does not normalize gates, classify bindings, or split guards. Gates
become Rete.IR.Gate placeholders. :join_filter, :join_bind, and :new_bind are
left nil.
{:type, a, b, ...} fact pattern of any arity, including {:type}
%Mod{f: v} struct fact pattern, type is the module
%{__type__: :type, f: v} tagged map fact pattern
f = <pattern> bind the whole fact to f
<pattern> when <guard> per condition guard
[<pattern>] collection binding (collect all), anonymous
c = [<pattern> when <guard>] collection binding, bound, with a guard
{gate, [element, ...]} gate, gate in [:and, :or, :not, :nand, :nor, :xor, :xnor]A leading %{...} literal is the options map, not a fact pattern. A rule level guard
becomes a trailing Rete.IR.Test.
Expression codes stay stable across compilations of the same source. This is what lets
the network share nodes. The compiler qualifies module attributes with the defining
module before hashing, so the same pattern in two modules with different attribute
values gets different codes. See docs/design/ir.md §5.
Summary
Functions
Builds the alpha Rete.IR.Expr of a condition.
Builds the Rete.IR.Expr of a test over bindings only.
Compiles a fact pattern into {type, argument_pattern}.
Resolves every alias and __MODULE__ in the AST to the module it names.
Quoted definitions of every expression function of a production.
Collects the variables bound by a pattern.
Parses a single LHS element into a condition struct.
Parses a production declaration and body into a Rete.IR.Production.
Replaces compile-time constants in the AST with their values.
Types
@type env() :: Macro.Env.t()
The Macro.Env of the caller of defrule/defquery.
Functions
@spec build_alpha_expr( atom() | module(), Macro.t(), Macro.t(), Macro.t() | nil, %{required(atom()) => Macro.t()} ) :: Rete.IR.Expr.t()
Builds the alpha Rete.IR.Expr of a condition.
pattern is the raw pattern as written — only used to compute the stable hash.
args_ast is the compiled argument pattern from compile_pattern/2. guard is the
per-condition guard AST, or nil. bind maps every bound variable to its AST.
The generated function returns the bindings map when the fact matches and the guard
holds, and nil otherwise. This delegates to Rete.DSL.Codegen.alpha_expr/5, which
owns the naming and hashing scheme.
@spec build_test_expr(Macro.t(), %{required(atom()) => Macro.t()}) :: Rete.IR.Expr.t()
Builds the Rete.IR.Expr of a test over bindings only.
The generated function takes the bindings map and returns the value of the
guard. Delegates to Rete.DSL.Codegen.test_expr/2.
Compiles a fact pattern into {type, argument_pattern}.
The argument pattern is what the generated alpha function matches the fact against. It
never checks the fact type. The tag slot of a tuple becomes _. A struct pattern loses
its __struct__ check. A tagged map pattern loses its __type__ key. Type filtering,
including taxonomy, happens later, when the alpha index decides whether to propagate a
fact to a node.
Resolves every alias and __MODULE__ in the AST to the module it names.
Expression codes are shared across modules, so two conditions with the same code must
have the same behaviour. An alias is lexical. H.ok?(amt) is the same AST in two
modules that alias H to different things. Hashing it unresolved would give both the
same code, and let Rete.get_expr_data/1 collapse them onto whichever function it saw
first. Resolving the alias before hashing makes the code depend on the module actually
called instead.
Only alias nodes are expanded, never macros — the body has to reach the generated function exactly as the user wrote it.
@spec expr_defs(Rete.IR.Production.t()) :: [Macro.t()]
Quoted definitions of every expression function of a production.
Emit these into the module body before escaping the production. Delegates to
Rete.DSL.Codegen.expr_defs/1, which owns code generation.
Collects the variables bound by a pattern.
Returns %{name => variable_ast}. Pinned values (^x), module attributes (@x), and
variables whose name starts with _ are not bindings, and this excludes them. It also
excludes anything a nested construct binds for itself. This delegates to
Rete.DSL.Vars.pattern_vars/1, which owns scope analysis.
@spec parse_element(env(), Macro.t()) :: Rete.IR.condition()
Parses a single LHS element into a condition struct.
Exposed so that later phases can re-parse fragments (for example the branches a gate is normalized into).
@spec parse_production(env(), Macro.t(), Macro.t(), :rule | :query) :: Rete.IR.Production.t()
Parses a production declaration and body into a Rete.IR.Production.
decl is the quoted call, e.g. r(%{salience: 1}, {:foo, id}) when id > 0. body is
the quoted do block, or nil. type is :rule or :query.
:rhs is nil on the result. It is captured when the production is escaped.
Replaces compile-time constants in the AST with their values.
This resolves both forms, because an LHS condition compiles into a standalone function in the ruleset module, and neither form survives being moved there.
The compiler qualifies @attr with the defining module, so the same pattern in two
modules with different attribute values does not share an expression. It cannot resolve
the value itself here: @attr expands to a call that only runs once the module body is
evaluated — after every macro in it has already expanded. So what distinguishes two uses
of one attribute is their line instead, which Rete.DSL.Codegen.ast_hash/1 keeps for
attribute nodes alone. Without that, @limit 5 and a later @limit 100 over the same
pattern would hash identically, and share one generated function.
^value has no enclosing scope to refer to, once the condition becomes its own
function. So the compiler unwraps each spelling. ^@limit and ^5 become the literal
value, since matching on ^5 and on 5 is the same match. ^amt becomes plain amt,
because sharing a variable between two conditions is already how this DSL spells a
join. Dropping the pin lets ordinary binding classification turn it into a join key.