Skip to content

Instantly share code, notes, and snippets.

@polvalente
Created August 16, 2020 21:38
Show Gist options
  • Save polvalente/284641f9e9c29fb826e34c0439b07482 to your computer and use it in GitHub Desktop.
Save polvalente/284641f9e9c29fb826e34c0439b07482 to your computer and use it in GitHub Desktop.
Multi-comparator expression parser
defmodule MultiBoolComp do
@moduledoc """
Defines an `sequential_bool` macro that parses a sequence of boolean comparisons in-line, such as:
`sequential_bool(a < b >= c == d <= e)` which would be equivalent to `a < b and b >= c and c == d and d <= e`
Only the native comparators are accepted (<, >, <=, >=, ==, !=, ===, !==)
### Examples:
iex> MultiBoolComp.sequential_bool(1 <= 4)
true
iex> MultiBoolComp.sequential_bool(1 < 2 == 2 < 4 != 6 >= 5 <= 7)
true
iex> MultiBoolComp.sequential_bool(1 < 2 == 3 < 4 != 6 >= 5 <= 7)
false
"""
defmacro sequential_bool(ast) do
# IO.inspect(ast, label: "original ast")
{_node, flattened_ast} = flatten_bool_ast(ast)
# IO.inspect(flattened_ast, label: "flattened_ast")
esc_flattened_ast = Macro.escape(flattened_ast)
quote location: :keep do
MultiBoolComp.run(unquote(esc_flattened_ast))
end
end
def run(%{args: [nil]}), do: false
def run(%{operators: []}), do: true
def run(%{operators: [op | op_tl], args: [l, r | arg_tl]}) do
# IO.puts("Evaluating #{l} #{op} #{r} to: #{apply(Kernel, op, [l, r])}")
if apply(Kernel, op, [l, r]) do
run(%{operators: op_tl, args: [r | arg_tl]})
else
false
end
end
@doc """
Flattens a boolean-expression AST, by transforming all boolean function calls into
A single function call to `MultiBoolComp.run/1`
"""
def flatten_bool_ast(ast) do
Macro.prewalk(ast, %{operators: [], args: []}, fn
{op, env, [l, r]}, acc when op in ~w(== != === !== <= >=)a ->
{_, flattened_l} = flatten_bool_ast(l)
{_, flattened_r} = flatten_bool_ast(r)
operators = flattened_l.operators ++ [op] ++ flattened_r.operators
args = flattened_l.args ++ flattened_r.args
updated = acc |> Map.put(:args, args) |> Map.put(:operators, operators)
# we return a node without arguments because the
# flatten_bool_ast/1 calls have already parsed our arg nodes
{{op, env, []}, updated}
{op, _env, _} = node, acc ->
{node, Map.put(acc, :operators, acc.operators ++ [op])}
val, acc ->
{val, Map.put(acc, :args, acc.args ++ [val])}
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment