Rete.IR (Rete v0.2.0)

Copy Markdown View Source

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 time

Every 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

Types

A single LHS condition.

One element of a left hand side.

The left hand side of a production.

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

condition()

A single LHS condition.

element()

@type element() :: condition() | {:or, [[element()]]}

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.

lhs()

@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

bound_vars(coll)

@spec bound_vars(condition()) :: [atom()]

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]}})
[]

escape(production)

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

expr_data(production)

@spec expr_data(Rete.IR.Production.t()) :: [{atom(), (... -> any())}]

The {code, fun} pairs of every expression of a production.

exprs(arg1)

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

lhs_bindings(lhs)

@spec lhs_bindings(lhs()) :: {[atom()], [atom()]}

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]}

rhs_name(name)

@spec rhs_name(atom()) :: atom()

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.