Current section
Files
Jump to
Current section
Files
lib/ord/dsl/executor.ex
defmodule Funx.Ord.Dsl.Executor do
@moduledoc false
# Compile-time code generator that converts parsed DSL nodes into quoted AST.
#
# ## Type-Specific Code Generation
#
# Uses type information from parser to generate specific code paths without
# runtime branching. Each Step type gets specialized handling:
#
# - `:projection` - Optics/functions → wrap in contramap
# - `:module_ord` - Module with compare/2 → convert via to_ord_map
# - `:ord_map` - Behaviour returning Ord map → use directly
# - `:dynamic` - 0-arity helper → runtime type detection
# - `:ord_variable` - Variable holding ord map → runtime validation
#
# Pattern matching on the `type` field at compile time eliminates unreachable
# case branches, avoiding compiler warnings.
alias Funx.Monoid.Ord
alias Funx.Ord
alias Funx.Ord.Dsl.Step
@doc """
Execute (compile) a list of steps into quoted code that builds an Ord.
Unlike Maybe DSL's runtime executor, this runs at compile time and returns
quoted AST that will be compiled into the calling module.
## Execution Model
Each Step is converted to:
- `:asc` → `contramap(projection, ord)`
- `:desc` → `reverse(contramap(projection, ord))`
Multiple steps are combined with `compose([...])` (monoid append).
If two values are equal on all specified fields, they compare as equal.
Users can add an explicit tiebreaker if needed (e.g., `asc &Function.identity/1`).
"""
@spec execute_steps(list(Step.t())) :: Macro.t()
def execute_steps([]), do: empty_ord_ast()
def execute_steps([step]), do: step_to_ord_ast(step)
def execute_steps(steps), do: build_concat_ast(steps)
# ============================================================================
# AST BUILDING
# ============================================================================
defp build_concat_ast(steps) do
ord_asts = Enum.map(steps, &step_to_ord_ast/1)
quote do
Ord.compose([unquote_splicing(ord_asts)])
end
end
# === Projection type ===
# Optics or functions - wrap in contramap
defp step_to_ord_ast(%Step{
direction: direction,
projection: projection_ast,
ord: ord_ast,
type: :projection
}) do
base_ord_ast =
quote do
Ord.contramap(unquote(projection_ast), unquote(ord_ast))
end
case direction do
:asc -> base_ord_ast
:desc -> build_reverse_ast(base_ord_ast)
end
end
# === Module with compare/2 ===
# Convert module to Ord map using to_ord_map
defp step_to_ord_ast(%Step{direction: direction, projection: module_ast, type: :module_ord}) do
base_ord_ast =
quote do
Ord.to_ord_map(unquote(module_ast))
end
case direction do
:asc -> base_ord_ast
:desc -> build_reverse_ast(base_ord_ast)
end
end
# === Ord map from behaviour ===
# Use the Ord map directly (no contramap needed)
defp step_to_ord_ast(%Step{direction: direction, projection: ord_map_ast, type: :ord_map}) do
base_ord_ast = ord_map_ast
case direction do
:asc -> base_ord_ast
:desc -> build_reverse_ast(base_ord_ast)
end
end
# === Dynamic type ===
# Runtime detection for 0-arity helpers
defp step_to_ord_ast(%Step{
direction: direction,
projection: projection_ast,
ord: ord_ast,
type: :dynamic
}) do
base_ord_ast =
quote do
projection = unquote(projection_ast)
case projection do
%{compare: compare_fun} when is_function(compare_fun, 2) ->
# Already an Ord map - use it directly
projection
module when is_atom(module) ->
# It's a module - convert to Ord map
Ord.to_ord_map(module)
_ ->
# It's a projection - wrap in contramap
Ord.contramap(projection, unquote(ord_ast))
end
end
case direction do
:asc -> base_ord_ast
:desc -> build_reverse_ast(base_ord_ast)
end
end
# === Ord variable ===
# Runtime validation of ord map variable
defp step_to_ord_ast(%Step{direction: direction, projection: var_ast, type: :ord_variable}) do
base_ord_ast =
quote do
ord_var = unquote(var_ast)
case ord_var do
%{lt?: lt_fun, le?: le_fun, gt?: gt_fun, ge?: ge_fun}
when is_function(lt_fun, 2) and is_function(le_fun, 2) and
is_function(gt_fun, 2) and is_function(ge_fun, 2) ->
# Valid ord map - use it directly
ord_var
_ ->
raise RuntimeError, """
Expected an Ord map, got: #{inspect(ord_var)}
An Ord map must have the following structure:
%{
lt?: fn(a, b) -> boolean end,
le?: fn(a, b) -> boolean end,
gt?: fn(a, b) -> boolean end,
ge?: fn(a, b) -> boolean end
}
You can create ord maps using:
- ord do ... end
- Ord.contramap(...)
- Ord.reverse(...)
- Ord.compose([...])
"""
end
end
case direction do
:asc -> base_ord_ast
:desc -> build_reverse_ast(base_ord_ast)
end
end
# ============================================================================
# HELPERS
# ============================================================================
defp build_reverse_ast(ord_ast) do
quote do
Ord.reverse(unquote(ord_ast))
end
end
# Monoid identity: everything compares equal
defp empty_ord_ast do
quote do
%Funx.Monoid.Ord{}
end
end
end