Current section

Files

Jump to
funx lib predicate dsl executor.ex
Raw

lib/predicate/dsl/executor.ex

defmodule Funx.Predicate.Dsl.Executor do
@moduledoc false
# Compile-time code generator that converts parsed DSL nodes into quoted AST.
#
# ## Architecture
#
# The executor is the second phase of DSL compilation:
# 1. Parser - Normalizes syntax → Step/Block nodes
# 2. Executor (this module) - Converts nodes → quoted runtime code
# 3. Runtime - Executes compiled predicate checks
#
# ## Code Generation
#
# The executor converts nodes to calls to existing Predicate functions:
#
# - Step (negate: false) → Pass through predicate AST
# - Step (negate: true) → `Predicate.p_not(predicate)`
# - Block (all) → `Predicate.p_all([children...])`
# - Block (any) → `Predicate.p_any([children...])`
#
# ## Tree Walking
#
# The executor recursively walks the node tree, generating code for each node.
# Top-level nodes are implicitly combined with p_all (AND logic).
alias Funx.Predicate
alias Funx.Predicate.Dsl.{Block, Step}
@doc """
Execute (compile) a list of nodes into quoted code that builds a predicate.
## Execution Model
Each node is converted to:
- Step (negate: false) → predicate AST
- Step (negate: true) → `Predicate.p_not(predicate)`
- Block (all) → `Predicate.p_all([children...])`
- Block (any) → `Predicate.p_any([children...])`
Top-level nodes are combined with `p_all` (implicit all strategy).
"""
@spec execute_nodes(list(Step.t() | Block.t())) :: Macro.t()
def execute_nodes([]), do: empty_predicate_ast()
def execute_nodes([single_node]), do: node_to_ast(single_node)
def execute_nodes(nodes), do: build_all_ast(nodes)
# === Block combinators ===
defp build_all_ast(nodes) do
pred_asts = Enum.map(nodes, &node_to_ast/1)
quote do
Predicate.p_all([unquote_splicing(pred_asts)])
end
end
defp build_any_ast(nodes) do
pred_asts = Enum.map(nodes, &node_to_ast/1)
quote do
Predicate.p_any([unquote_splicing(pred_asts)])
end
end
# === Step nodes ===
# Bare step (no projection), non-negated - pass through predicate AST
defp node_to_ast(%Step{type: :bare, predicate: predicate_ast, negate: false}) do
predicate_ast
end
# Bare step (no projection), negated - wrap with p_not
defp node_to_ast(%Step{type: :bare, predicate: predicate_ast, negate: true}) do
quote do
Predicate.p_not(unquote(predicate_ast))
end
end
# Step with projection, non-negated - compose projection with predicate
defp node_to_ast(%Step{
type: :projection,
projection: projection_ast,
predicate: predicate_ast,
negate: false
}) do
quote do
Predicate.compose_projection(unquote(projection_ast), unquote(predicate_ast))
end
end
# Step with projection, negated - compose and negate
defp node_to_ast(%Step{
type: :projection,
projection: projection_ast,
predicate: predicate_ast,
negate: true
}) do
quote do
Predicate.p_not(
Predicate.compose_projection(unquote(projection_ast), unquote(predicate_ast))
)
end
end
# Behaviour step, non-negated - return the predicate from Module.pred(opts)
defp node_to_ast(%Step{type: :behaviour, predicate: behaviour_ast, negate: false}) do
behaviour_ast
end
# Behaviour step, negated - wrap with p_not
defp node_to_ast(%Step{type: :behaviour, predicate: behaviour_ast, negate: true}) do
quote do
Predicate.p_not(unquote(behaviour_ast))
end
end
# === Block nodes ===
defp node_to_ast(%Block{strategy: :all, children: children}) do
build_all_ast(children)
end
defp node_to_ast(%Block{strategy: :any, children: children}) do
build_any_ast(children)
end
# === Empty predicate ===
# Empty predicate always returns true (identity for AND)
defp empty_predicate_ast do
quote do
fn _ -> true end
end
end
end