The intermediate representation (IR) shared by every compile phase of Rete.
Internal. Rete.DSL.Parser produces it. Later phases refine it in place. It is
escaped into the defining module, for the network builder to read at runtime.
quoted DSL
|> Rete.DSL.Parser.parse_production/4 # AST -> IR
|> Rete.DSL.Normalize.normalize_lhs/1 # gates -> conditions
|> Rete.Compiler.Sort.sort/1 # topological condition order
|> Rete.DSL.Bindings.classify/2 # join keys, guard splitting
|> Rete.IR.escape/1 # emitted into the ruleset module
|> Rete.Compiler.build/2 # the network, at build timeEvery phase consumes and produces %Rete.IR.Production{}. So a struct carries fields
a later phase fills in. Each struct records which fields are nil after parsing.
:__ast__ holds the raw quoted fragments the later phases need. It is compile-time
only. escape/1 drops it, so quoted AST never reaches the compiled module.
A condition's :type is the declared fact type. It is never a runtime check baked
into the alpha expression โ the alpha matches a fact of any type, on purpose. The alpha
index applies the taxonomy instead. See docs/design/ir.md ยง2.
Summary
Functions
All variables a condition makes visible downstream.
Turns a parsed production into quoted code that rebuilds it inside the defining module.
The {code, fun} pairs of every expression of a production.
Every Rete.IR.Expr reachable from a production or condition, in LHS order.
The variables a classified LHS makes visible to the right hand side, split
into {guaranteed, optional}.
The name of the function a production's right hand side compiles to.
Types
@type condition() :: Rete.IR.Fact.t() | Rete.IR.Coll.t() | Rete.IR.Test.t() | Rete.IR.Gate.t() | Rete.IR.Negation.t() | Rete.IR.CompoundNegation.t()
A single LHS condition.
One element of a left hand side.
This is either a single condition, or a disjunction of conjunctions. A branch is itself a list of elements, so branches may nest.
@type lhs() :: [element()]
The left hand side of a production.
An ordered list, never flattened to DNF, since that explodes combinatorially. A
disjunction fans out from the current parents, and re-converges before the next
element. The parser emits only plain conditions and Rete.IR.Gate placeholders.
Functions
All variables a condition makes visible downstream.
:bind plus the fact or collection binding. A Rete.IR.Test and a negation bind
nothing.
iex> Rete.IR.bound_vars(%Rete.IR.Fact{bind: [:id], fact_binding: :f})
[:id, :f]
iex> Rete.IR.bound_vars(%Rete.IR.Negation{condition: %Rete.IR.Fact{bind: [:id]}})
[]
@spec escape(Rete.IR.Production.t()) :: Macro.t()
Turns a parsed production into quoted code that rebuilds it inside the defining module.
Splice the result into the module body after the expression functions are
defined, since it captures them by name. This drops :__ast__. :fun and :rhs
become Function.capture/3 calls. :opts stays unescaped, so option values get
evaluated in the module scope.
@spec expr_data(Rete.IR.Production.t()) :: [{atom(), (... -> any())}]
The {code, fun} pairs of every expression of a production.
@spec exprs(Rete.IR.Production.t() | element()) :: [Rete.IR.Expr.t()]
Every Rete.IR.Expr reachable from a production or condition, in LHS order.
An alpha comes before the join filter of the same condition. This is not
deduplicated. Use Enum.uniq_by(& &1.name) when emitting functions.
The variables a classified LHS makes visible to the right hand side, split
into {guaranteed, optional}.
Both lists are sorted and disjoint, and together they make up the production's
:bind. A guaranteed binding is in every token that reaches the RHS. An
optional one is bound on some branch of a disjunction but not all, so the RHS
reads it with Map.get/2.
Run this on the classified LHS. On a raw parsed one, it answers with the union of
a Rete.IR.Gate's arguments โ the over-approximation it exists to avoid.
iex> alias Rete.IR
iex> user = %IR.Fact{bind: [:id]}
iex> admin = %IR.Fact{bind: [:level]}
iex> IR.lhs_bindings([{:or, [[user], [admin]]}])
{[], [:id, :level]}
The name of the function a production's right hand side compiles to.
iex> Rete.IR.rhs_name(:loyalty)
:__rhs_loyalty__Deliberately not the production's own name. A query is called by name, so the name has to stay free for the arity-2 function that runs it.