defmodule Iteraptor.AST do
@moduledoc """
`Iteraptor.AST` module traverses AST, allowing `map`, `reduce` and family.
"""
@doc """
Mapper for the AST.
## Parameters
- `input`: the AST to traverse
- `fun`: the function to be called on the tree element
- `opts`: the options to be passed to the iteration
- `yield`: `:all | nil` what to yield; _default:_ `nil`
for yielding _values only_.
## Examples
iex> bindings = [a: 1, b: 2, c: 3]
...> ":math.sin(42 * a / (3.14 * b)) > c"
...> |> Iteraptor.AST.map(fn
...> {var, _, val} when is_atom(val) -> bindings[var]
...> any -> any
...> end)
{:>, [line: 1],
[
{{:., [line: 1], [:math, :sin]}, [line: 1],
[
{:/, [line: 1],
[
{:*, [line: 1], [42, 1]},
{:*, [line: 1], [3.14, 2]}
]}
]},
3
]}
"""
@spec map(binary() | {atom(), list(), any()} | list(), (any(), any() -> any()), list()) :: any()
def map(input, fun, opts \\ [])
def map(input, fun, opts) when is_binary(input) do
with {:ok, ast} <- Code.string_to_quoted(input), do: map(ast, fun, opts)
end
def map(input, fun, opts) do
with {ast, _} <-
Macro.postwalk(input, [], fn
{var, meta, ast}, acc when not is_list(ast) and is_atom(var) ->
{fun.({var, meta, ast}), acc}
e, acc ->
if opts[:yield] == :all, do: {fun.(e), acc}, else: {e, acc}
end),
do: ast
end
@doc """
Reduces the AST with an accumulator.
## Parameters
- `input`: the AST to traverse
- `acc`: the accumulator
- `fun`: the function to be called on the tree element
- `opts`: the options to be passed to the iteration
- `yield`: `:all | nil` what to yield; _default:_ `nil`
for yielding _values only_.
## Examples
iex> ":math.sin(42 * a / (3.14 * b)) > c"
...> |> Iteraptor.AST.reduce([], fn
...> {var, _, val}, acc when is_atom(val) -> [var | acc]
...> _, acc -> acc
...> end)
...> |> Enum.reverse()
~w|a b c|a
"""
@spec reduce(
binary() | {atom(), list(), any()} | list(),
any(),
(any(), any() -> any()),
list()
) :: any()
def reduce(input, acc, fun, opts \\ []), do: do_traverse(input, acc, fun, opts)
##############################################################################
defp do_traverse(string, acc, fun, opts) when is_binary(string) do
with {:ok, ast} <- Code.string_to_quoted(string),
do: do_traverse(ast, acc, fun, opts)
end
defp do_traverse({var, meta, val}, acc, fun, _opts)
when is_atom(var) and is_atom(val),
do: fun.({var, meta, val}, acc)
defp do_traverse({operator, meta, ast}, acc, fun, opts) do
acc =
Iteraptor.reduce(
ast,
acc,
fn
{_, {term, meta, ast}}, acc when is_list(ast) ->
acc = do_traverse(ast, acc, fun, opts)
if opts[:yield] == :all, do: fun.({term, meta, ast}, acc), else: acc
{_, {var, meta, ast}}, acc when is_atom(var) ->
fun.({var, meta, ast}, acc)
_, acc ->
acc
end,
opts
)
if opts[:yield] == :all || is_atom(ast), do: fun.({operator, meta, ast}, acc), else: acc
end
defp do_traverse(ast, acc, fun, opts) when is_list(ast) do
# fun =
# if opts[:yield] == :all do
# &do_traverse(&1, fun.(&1, &2), fun, opts)
# else
# &do_traverse(&1, &2, fun, opts)
# end
Enum.reduce(ast, acc, &do_traverse(&1, &2, fun, opts))
end
defp do_traverse(term, acc, fun, opts) do
if opts[:yield] == :all, do: fun.(term, acc), else: acc
end
end