plymio_ast v0.2.0 Plymio.Ast.Transform
Utility Functions for Transforming Asts (Quoted Forms).
transform/2
implements a dsl to transform the ast.
Applying the Transform DSL
transform/2
take the ast (Macro.t
) together with a set of dsl options (Keyword
) where the key is the transform verb (e.g. :pipe_before
- see later) and the value the data needed by the verb.
The dsl options define a pipeline of transformations sequentially applied to the original ast.
Many of the dsl verbs are implemented by functions in Plymio.Ast.Utils
.
Helper Functions for the Doctests
The tests below use helper functions.
One helper, helper_transform_eval/3
,
applies the dsl (transform/2
) and run the returned ast
(Code.eval_quoted/3
) with the supplied binding. The helper returns
a 2tuple with the result of the eval and the visualised
(Macro.to_string/1
) code. The helper looks like this:
def helper_transform_eval(binding, ast, dsl_opts) do
ast = ast |> Plymio.Ast.Transform.transform(dsl_opts)
result = ast |> Code.eval_quoted(binding, __ENV__) |> elem(0)
{result, ast |> Macro.to_string}
end
Transform DSL Verbs
The dsl implements a number of verbs given in the dsl_options
(Keyword
).
DSL Verb - :pipe_before
The :pipe_before
verb pipes the main ast argument before the ast(s) given as the value of verb.
This example pipes a var (x
) into a call to an anonymous function that adds 1:
iex> ast = Macro.var(:x, nil)
...> before_ast = quote(do: fn v -> v + 1 end.())
...> [x: 42] # binding
...> |> helper_transform_eval(ast, pipe_before: before_ast)
{43, "x |> (fn v -> v + 1 end).()"}
This example supplies a list of asts to the :pipe_before
verb:
iex> ast = Macro.var(:x, nil)
...> before_ast1 = quote(do: fn v -> v + 1 end.())
...> before_ast2 = quote(do: fn v -> v * v end.())
...> before_ast3 = quote(do: fn v -> 1 - v end.())
...> [x: 42]
...> |> helper_transform_eval(ast,
...> pipe_before: [before_ast1, before_ast2, before_ast3])
{-1848, "x |> (fn v -> v + 1 end).() |> (fn v -> v * v end).() |> (fn v -> 1 - v end).()"}
Implicit in the examples above has been that the left side ast of
the pipe (|>
) should become the zeroth argument of the right side
ast. However the call to Macro.pipe/3
that does the piping takes
the index (Integer
- zero offset) to use (in the example above it was set automatically to zero).
To specify the pipe index, any of the asts can be a 2tuple where the first element is the “pure” ast and the second the pipe index.
These two simple examples of a subtraction show why the pipe index
is important: The first uses the default index of zero (so the code
becomes x - 100
) while the second supplies an index of 1 (so the code
becomes 100 - x
).
When the index is zero, a
left |> right
ast is generated (first example), otherwise the generated ast inserts the left ast directly in the right ast at the index. This is just to make the code, afterMacro.to_string/1
, visually more obvious.
iex> ast = Macro.var(:x, nil)
...> before_ast = quote(do: Kernel.-(100))
...> [x: 42]
...> |> helper_transform_eval(ast, pipe_before: before_ast)
{-58, "x |> Kernel.-(100)"}
iex> ast = Macro.var(:x, nil)
...> before_ast = {quote(do: Kernel.-(100)), 1}
...> [x: 42]
...> |> helper_transform_eval(ast, pipe_before: before_ast)
{58, "Kernel.-(100, x)"}
DSL Verb - :pipe_after
The :pipe_after
verb pipes the main ast argument after the
ast(s) given as the value of the :pipe_after
.
This is the same example as above that pipes a var (x
) into a call
to an anonymous function that adds 1 but the arguments are
switched around:
iex> ast = quote(do: fn v -> v + 1 end.())
...> after_ast = Macro.var(:x, nil)
...> [x: 42]
...> |> helper_transform_eval(ast, pipe_after: after_ast)
{43, "x |> (fn v -> v + 1 end).()"}
Again, the same example above that supplies a list of asts to the
:pipe_before
verb but with the arguments switched around:
iex> ast = quote(do: fn v -> 1 - v end.())
...> after_ast1 = Macro.var(:x, nil)
...> after_ast2 = quote(do: fn v -> v + 1 end.())
...> after_ast3 = quote(do: fn v -> v * v end.())
...> [x: 42]
...> |> helper_transform_eval(ast,
...> pipe_after: [after_ast1, after_ast2, after_ast3])
{-1848, "x |> (fn v -> v + 1 end).() |> (fn v -> v * v end).() |> (fn v -> 1 - v end).()"}
:pipe_after
also support supplying an index for any ast. This
somewhat complicated example demonstrates using multiple asts with
indicies:
iex> ast = Macro.var(:x, nil)
...> [x: 7] # binding
...> |> helper_transform_eval(ast, pipe_before: [
...> {quote(do: Kernel./(42)), 1}, # index is 1
...> quote(do: List.wrap),
...> {quote(do: Kernel.++([1,2,3])), 1}, # index is 1
...> {quote(do: Enum.reduce([&Enum.sum/1, fn v -> v * v end], fn f, s -> f.(s) end)), 1},
...> ])
{144.0, "Enum.reduce([&Enum.sum/1, fn v -> v * v end], Kernel.++([1, 2, 3], Kernel./(42, x) |> List.wrap()), fn f, s -> f.(s) end)"}
DSL Verb - :postwalk
:postwalk
runs Macro.postwalk/2
or Macro.postwalk/3
depending
on whether the verb’s value is a either function of arity one, or a
2tuple where the first element is the accumulator and the second a
function of arity two.
This simple example just changes the x var ({:x, [], nil}
) to y ({:y, [], nil}
).
iex> ast = Macro.var(:x, nil)
...> [y: 99]
...> |> helper_transform_eval(ast, postwalk:
...> fn {:x, [], nil} -> Macro.var(:y, nil)
...> # passthru
...> any -> any
...> end)
{99, "y"}
This more involved example returns an {ast, acc}
2tuple where the
acc(umulator) is a count of how often the x var was used. Refer to
Macro.postwalk/3
for explanation of an accumulator function.
iex> ast = quote do
...> x = x + x
...> x = x * x
...> x = x - x
...> end
...> {_, acc} = ast
...> |> helper_transform(postwalk: {
...> 0, # the initial accumulator
...> fn {:x, [], _} = ast, acc -> {ast, acc + 1}
...> # passthru
...> any, acc -> {any, acc}
...> end})
...> match?(9, acc)
true
DSL Verb - :prewalk
:prewalk
runs Macro.prewalk/2
or Macro.prewalk/3
depending
on whether the verb’s value is a either function of arity one, or a
2tuple where the first element is the accumulator and the second a
function of arity two.
In many situations, :prewalk
and :postwalk
do the same thing: this is the :postwalk
example rewritten to use :prewalk
:
iex> ast = Macro.var(:x, nil)
...> [y: 99]
...> |> helper_transform_eval(ast, prewalk:
...> fn {:x, [], nil} -> Macro.var(:y, nil)
...> # passthru
...> any -> any
...> end)
{99, "y"}
DSL Verb - :transform
The :transform
verb applies an arbitrary function to the current ast / value.
This example retrieves the ast from an {ast, acc}
2tuple from the example above.
iex> quote do
...> x = x + x
...> x = x * x
...> x = x - x
...> end
...> |> helper_transform(postwalk: {
...> 0, # the initial accumulator
...> fn {:x, [], _} = ast, acc -> {ast, acc + 1}
...> # passthru
...> any, acc -> {any, acc}
...> end},
...> transform: fn {ast, _acc} -> ast end)
...> |> Macro.to_string
...> |> fn str -> Regex.replace(~r/\n\s*/, str, "|") end.() # tidy code string
"(|x = x + x|x = x * x|x = x - x|)"
DSL Verb - :splice
The :splice
verb is akin to
Kernel.SpecialForms.unquote_splicing/1
where the one or more asts
given as the verb’s value are inserted consecutively into the
arguments of the main ast.
If main ast has an index (e.g. {ast,index}
), the insertion starts at the index, else zero.
This example splices the first thrre arguments into an anonymous function call, starting at the zeroth argument. Note the anon function call in the ast is defined as a “partial” call with the last three arguments already provided.
iex> ast = quote(do: fn a,b,c,x,y,z -> [a,b,c,x,y,z] end.(:this_is_x, :y, "z"))
...> args012 = [:a, {:b21, :b22}, %{c: 3}] |> Enum.map(&Macro.escape/1)
...> [] |> helper_transform_eval(ast, splice: args012)
{[:a, {:b21, :b22}, %{c: 3}, :this_is_x, :y, "z"],
"(fn a, b, c, x, y, z -> [a, b, c, x, y, z] end).(:a, {:b21, :b22}, %{c: 3}, :this_is_x, :y, \"z\")"}
This example specifies the index ({ast, 2}
) to start the splicing at the 3rd argument:
iex> ast = quote(do: fn a,b,c,x,y,z -> [a,b,c,x,y,z] end.(:a, {:b21, :b22}, "z"))
...> args234 = [%{c: 3}, :this_is_x, :y] |> Enum.map(&Macro.escape/1)
...> [] |> helper_transform_eval({ast, 2}, splice: args234)
{[:a, {:b21, :b22}, %{c: 3}, :this_is_x, :y, "z"],
"(fn a, b, c, x, y, z -> [a, b, c, x, y, z] end).(:a, {:b21, :b22}, %{c: 3}, :this_is_x, :y, \"z\")"}
This example show how a named function’s signature could be spliced.
Remember though that the arguments to a function are the arguments in the
first argument in the def
ast. For example the quoted fun1 def
below looks like:
{:def,
[context: Elixir, import: Kernel],
[{:fun1, [context: Elixir],
# here are fun1's arguments
[{:a, [], Elixir}, {:b, [], Elixir}, {:c, [], Elixir}]},
[do: {:+, [context: Elixir, import: Kernel],
[{:+, [context: Elixir, import: Kernel],
[{:a, [], Elixir}, {:b, [], Elixir}]}, {:c, [], Elixir}]}]]}
The example uses a :postwalk
function to (recursively) call transform/1
to splice the argumnets into the {:fun1, ctx, args}
ast.
iex> arg0_ast = quote(do: a)
...> arg1_ast = quote(do: b \\ 9)
...> arg2_ast = quote(do: c \\ 42)
...> signature = [arg0_ast, arg1_ast, arg2_ast]
...> quote do
...> def fun1() do
...> a + b + c
...> end
...> end
...> |> helper_transform(postwalk: fn
...> {:fun1, _, _} = fun1_ast -> fun1_ast |> transform(splice: signature)
...> x -> x # passthru
...> end)
...> |> Macro.to_string
"def(fun1(a, b \\\\ 9, c \\\\ 42)) do\n a + b + c\nend"
Summary
Functions
transform/2
implements the dsl
Types
dsl_option :: {:transform, dsl_fun_transform} | {:splice, any} | {:pipe_before, dsl_ast_pure} | {:pipe_after, dsl_ast_pure}
Functions
transform/2
implements the dsl.
See the examples above of each individual option.