Rete.DSL.Parser (Rete v0.2.0)

Copy Markdown View Source

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

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

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

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

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

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.

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.