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, after Macro.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

Types

dsl_ast()
dsl_ast() :: dsl_ast_pure | dsl_ast_index
dsl_ast_index()
dsl_ast_index() :: {Macro.t, integer}
dsl_ast_pure()
dsl_ast_pure() :: Macro.t
dsl_ast_result()
dsl_ast_result() :: Macro.t
dsl_fun_transform()
dsl_fun_transform() :: (any -> any)
dsl_option()
dsl_option ::
  {:transform, dsl_fun_transform} |
  {:splice, any} |
  {:pipe_before, dsl_ast_pure} |
  {:pipe_after, dsl_ast_pure}
dsl_options()
dsl_options() :: [dsl_option]

Functions

transform(ast, opts \\ [])
transform(dsl_ast, dsl_options) :: dsl_ast

transform/2 implements the dsl.

See the examples above of each individual option.