Rete.DSL.Parser (Rete v0.1.0)

Copy Markdown View Source

Turns the quoted arguments of Rete.Ruleset.defrule/2 and Rete.Ruleset.defquery/2 into Rete.IR structs.

Internal. The first phase of the DSL front end. It records each LHS element's type, bindings and guard, builds the alpha and test Rete.IR.Expr descriptors, and 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, and :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 are stable across compilations of the same source, which is what lets the network share nodes. Module attributes are qualified 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

Types

The Macro.Env of the caller of defrule/defquery.

Functions

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

env()

@type env() :: Macro.Env.t()

The Macro.Env of the caller of defrule/defquery.

Functions

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

@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 (it is 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, and 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. Delegates to Rete.DSL.Codegen.alpha_expr/5, which owns the naming and hashing scheme.

build_test_expr(guard, bind)

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

compile_pattern(env, pattern)

@spec compile_pattern(env(), Macro.t()) :: {atom() | module(), Macro.t()}

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 and a tagged map pattern loses its __type__ key. Type filtering, including taxonomy, happens when the alpha index decides whether to propagate a fact to a node.

expand_aliases(ast, env)

@spec expand_aliases(Macro.t(), env()) :: Macro.t()

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, and 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.

Only alias nodes are expanded, never macros: the body has to reach the generated function as the user wrote it.

expr_defs(production)

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

parse_bind(ast)

@spec parse_bind(Macro.t()) :: %{required(atom()) => Macro.t()}

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 are excluded, and so is anything a nested construct binds for itself. Delegates to Rete.DSL.Vars.pattern_vars/1, which owns scope analysis.

parse_element(env, element)

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

parse_production(env, decl, body, type)

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

resolve_constants(ast, env)

@spec resolve_constants(Macro.t(), env()) :: Macro.t()

Replaces compile time constants in the AST with their values.

Both forms are resolved because an LHS condition is compiled into a standalone function in the ruleset module, and neither survives being moved there.

@attr is qualified with the defining module, so the same pattern in two modules with different attribute values does not share an expression. The value itself cannot be resolved here: @attr expands to a call that only runs once the module body is evaluated, which is after every macro in it has expanded. What distinguishes two uses of one attribute is therefore their line, which Rete.DSL.Codegen.ast_hash/1 keeps for attribute nodes alone. Without it, @limit 5 and a later @limit 100 over the same pattern hashed identically and shared one generated function.

^value has no enclosing scope to refer to once the condition is its own function, so each spelling is unwrapped. ^@limit and ^5 become the literal, since matching on ^5 and on 5 are the same match. ^amt becomes amt, because sharing a variable between two conditions is already how this DSL spells a join, so dropping the pin lets ordinary binding classification turn it into a join key.