Rete.DSL.Codegen (Rete v0.1.0)

Copy Markdown View Source

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.

kindaritysignature

| :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

Types

The variable ASTs of the bindings an expression destructures.

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

bind()

@type bind() :: %{required(atom()) => Macro.t()}

The variable ASTs of the bindings an expression destructures.

Functions

alpha_expr(type, pattern, args_ast, guard, bind)

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

ast_hash(ast)

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

check_attr_values!(module, code, values)

@spec check_attr_values!(module(), atom(), keyword()) :: :ok

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.

compile(production)

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

expr_code(prefix, bind_keys, hash)

@spec expr_code([atom() | String.t()], [atom()], integer()) :: atom()

Joins a code prefix, the sorted variable names and the hash into an expression code.

expr_def(expr)

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

expr_defs(production)

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

expr_hash(args, body)

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

expr_name(code)

@spec expr_name(atom()) :: atom()

The name of the function generated for an expression code, :"__<code>__".

join_filter_expr(type, local, guard)

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

query_def(production)

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

rhs_def(production)

@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 a FunctionClauseError 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/2 in the body, because the tokens of the other branches genuinely do not carry the key. It is nil there.

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.

test_expr(guard, bind)

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

type_code(type)

@spec type_code(atom() | module()) :: String.t()

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.