plymio_ast v0.2.0 Plymio.Ast.Utils

Utility Functions for Asts (Quoted Forms)

Summary

Functions

ast_enumerate/1 takes an ast and, if a :__block__, “demultiplexes” the list of constituent asts, and returns the list

ast_from_mfa/1 creates an ast to implement the function call defined in an MFA 3tuple ({module,function,arguments})

ast_postwalk/2 runs Macro.postwalk/2 or Macro.postwalk/3 depending on whether the 2nd argument 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

ast_validate/1 runs Macro.validate/1 on the argument and if the result is not :ok raises an ArgumentError exception

asts_enumerate/1 takes zero (nil), one or more asts, passes each ast to ast_enumerate/1, and “flat_maps” the results

asts_group/ take one or more asts and returns a Enum.group_by/1 map using the first element of the tuple as the key

asts_pipe/1 takes one or more asts and uses Macro.pipe/3 to pipe them together and create a new ast

asts_reduce/1 takes one or more asts and reduces them to a single ast using Kernel.SpecialForms.unquote_splicing/1

asts_sort/2 take one or more asts together with an (optional) weight map and returns a list of asts with the lower weight ast earlier in the list

asts_sort_weight_default/0 returns the hardcoded “backstop” weight

asts_sort_weights_default/0 returns the default map used to sort asts

asts_validate!/1 validates a list of asts using ast_validate!/1, returns the asts or raises an ArgumentError exception on the first invalid one

Types

ast()
ast() :: Macro.t
ast_fun_or_acc_fun_tuple()
ast_fun_or_acc_fun_tuple ::
  atom |
  (ast -> ast) |
  {any, atom} |
  {any, (ast, any -> {ast, any})}
ast_or_ast_acc_tuple()
ast_or_ast_acc_tuple() :: ast | {ast, any}
ast_pipe()
ast_pipe() :: ast_pipe_pure | ast_pipe_index
ast_pipe_index()
ast_pipe_index() :: {Macro.t, integer}
ast_pipe_pure()
ast_pipe_pure() :: Macro.t
asts()
asts() :: ast | [ast]
asts_pipe()
asts_pipe() :: ast_pipe | [ast_pipe]
asts_sort_weight_key()
asts_sort_weight_key() :: atom
asts_sort_weight_map()
asts_sort_weight_map() :: %{optional(asts_sort_weight_key) => any}

Functions

ast_enumerate(ast)
ast_enumerate(ast) :: asts

ast_enumerate/1 takes an ast and, if a :__block__, “demultiplexes” the list of constituent asts, and returns the list.

If not a :__block__, the ast is returned in a list.

Examples

iex> 1 |> ast_enumerate
[1]

iex> :two |> ast_enumerate
[:two]

iex> quote do
...>   x = 1
...>   y = 2
...>   z = x + y
...> end
...> |> ast_enumerate
[quote(do: x = 1), quote(do: y = 2), quote(do: z = x + y)]

iex> %{a: 1} |> ast_enumerate
** (ArgumentError) expected an ast; got: %{a: 1}
ast_from_mfa(mfa)

ast_from_mfa/1 creates an ast to implement the function call defined in an MFA 3tuple ({module,function,arguments}).

Each argument is escaped if necessary (i.e. if not already a valid ast).

It can be thought of as the static / ast “equivalent” of Kernel.apply/1.

Examples

iex> {X1, :f1, []} |> helper_ast_from_mfa_to_string
"X1.f1()"

iex> {X1, :f1, [1, :two, "tre"]} |> helper_ast_from_mfa_to_string
"X1.f1(1, :two, \"tre\")"

iex> {X2, :f2, [{1, :two, "tre"}]} |> helper_ast_from_mfa_to_string
"X2.f2({1, :two, \"tre\"})"

iex> {X2, :f2, [{1, :two, "tre"} |> Macro.escape]} |> helper_ast_from_mfa_to_string
"X2.f2({1, :two, \"tre\"})"

iex> {X3, :f3, [%{a: 1, b: %{b: 2}, c: {3, :tre, "tre"}}]} |> helper_ast_from_mfa_to_string
"X3.f3(%{a: 1, b: %{b: 2}, c: {3, :tre, \"tre\"}})"

iex> {X3, :f3, [%{a: 1, b: %{b: 2}, c: {3, :tre, "tre"}} |> Macro.escape]} |> helper_ast_from_mfa_to_string
"X3.f3(%{a: 1, b: %{b: 2}, c: {3, :tre, \"tre\"}})"
ast_postwalk(ast, value \\ nil)

ast_postwalk/2 runs Macro.postwalk/2 or Macro.postwalk/3 depending on whether the 2nd argument 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.

If the second argument is nil, the call to Macro.postwalk is prempted and the ast returned unchanged.

Examples

This examples changes occurences of the x var to the a var.

iex> ast = quote do
...>   x = x + 1
...> end
...> |> ast_postwalk(fn
...>      {:x, _, _} -> Macro.var(:a, nil)
...>      # passthru
...>      x -> x
...> end)
...> [a: 42] # binding
...> |> helper_ast_eval(ast)
{43, "a = a + 1"}

This example changes x to a and uses an accumulator to count the occurences of the a var:

iex> {ast, acc} = quote do
...>   x = x + 1
...>   x = x * x
...>   x = x - 5
...> end
...> |> ast_postwalk(
...>     {0, fn
...>      {:x, _, _}, acc -> {Macro.var(:a, nil), acc + 1}
...>      # passthru
...>      x,s -> {x,s}
...> end})
...> [a: 42] # binding
...> |> helper_ast_eval(ast)
...> |> Tuple.insert_at(0, acc) # add the accumulator
{7, 1844, "(\n  a = a + 1\n  a = a * a\n  a = a - 5\n)"}
ast_prewalk(ast, value \\ nil)

ast_prewalk/2 takes the same arguments as ast_postwalk/2.

ast_validate!(ast)
ast_validate!(ast) :: ast | no_return

ast_validate/1 runs Macro.validate/1 on the argument and if the result is not :ok raises an ArgumentError exception.

Examples

iex> 1 |> ast_validate!
1

iex> nil |> ast_validate! # nil is a valid ast
nil

iex> [:x, :y] |> ast_validate!
[:x, :y]

iex> ast = {:x, :y} # this 2tuple is a valid ast without escaping
...> result = ast |> ast_validate!
...> match?(^result, ast)
true

iex> {:x, :y, :z} |> ast_validate!
** (ArgumentError) expected an ast, got: {:error, {:x, :y, :z}}

iex> %{a: 1, b: 2, c: 3} |> ast_validate! # map not a valid ast
** (ArgumentError) expected an ast, got: {:error, %{a: 1, b: 2, c: 3}}

iex> ast = %{a: 1, b: 2, c: 3} |> Macro.escape # escaped map is a valid ast
...> result = ast |> ast_validate!
...> match?(^result, ast)
true
asts_enumerate(asts)
asts_enumerate(asts) :: asts

asts_enumerate/1 takes zero (nil), one or more asts, passes each ast to ast_enumerate/1, and “flat_maps” the results.

Examples

iex> nil |> asts_enumerate
[]

iex> 1 |> asts_enumerate
[1]

iex> :two |> asts_enumerate
[:two]

iex> [1, nil, :two, nil, "tre"] |> asts_enumerate
[1, :two, "tre"]

iex> quote do
...>   x = 1
...>   y = 2
...>   z = x + y
...> end
...> |> asts_enumerate
[quote(do: x = 1), quote(do: y = 2), quote(do: z = x + y)]

iex> [quote do
...>   x = 1
...>   y = 2
...>   z = x + y
...> end,
...> nil,
...> quote(do: a = 42),
...> nil,
...> quote do
...>   b = 7
...>   c = a - b
...> end]
...> |> asts_enumerate
[quote(do: x = 1), quote(do: y = 2), quote(do: z = x + y),
 quote(do: a = 42), quote(do: b = 7), quote(do: c = a - b)]

iex> %{a: 1} |> asts_enumerate
** (ArgumentError) expected an ast; got: %{a: 1}
asts_group(asts)
asts_group(asts) :: map

asts_group/ take one or more asts and returns a Enum.group_by/1 map using the first element of the tuple as the key.

asts_pipe(asts \\ [])
asts_pipe(asts_pipe) :: ast

asts_pipe/1 takes one or more asts and uses Macro.pipe/3 to pipe them together and create a new ast.

Each ast in the list is passed to an Enum.reduce/3 function, together with the result (ast) of all the pipe operations to date (i.e. the accumulator).

The default behaviour is for the latest ast to become the zeroth argument in the accumulator ast (i.e. just as the left hand side of |> becomes the zeroth argument of the right hand side)

However the call to Macro.pipe/3 that does the piping takes the zero-offset index.

To specify the pipe index, any of the asts in the list can be a 2tuple where the first element is the “pure” ast and the second the pipe index. No index (i.e. just the “pure” ast) implies index 0.

When the index is zero, a left |> right ast is generated, otherwise the generated ast inserts the latest ast directly into the auumulator ast at the index. This is just to make the code, after Macro.to_string/1, visually more obvious.

Any nil asts in the list are ignored. An empty list returns nil.

Examples

This example show what happens when all the asts do not have an explicit index:

iex> ast = [
 ...> Macro.var(:x, nil),
 ...> quote(do: fn x -> x * x end.()),
 ...> quote(do: List.wrap)
 ...> ]
 ...> |> asts_pipe
 ...> [x: 42] |> helper_ast_eval(ast)
 {[1764], "x |> (fn x -> x * x end).() |> List.wrap()"}

This example show what happens when an index of 2 is used to insert the value of x (42) as the 3rd argument in the call to a “partial” anonymous function which already has the 1st, 2nd and 4th arguments.

iex> ast = [
 ...>   Macro.var(:x, nil),
 ...>   {quote(do: fn p, q, x, y -> [{y, x}, {p,q}] end.(:p, :this_is_q, "y")), 2},
 ...>   quote(do: Enum.into(%{}))
 ...> ]
 ...> |> asts_pipe
 ...> [x: 42] |> helper_ast_eval(ast)
 {%{:p => :this_is_q, "y" => 42},
  "(fn p, q, x, y -> [{y, x}, {p, q}] end).(:p, :this_is_q, x, \"y\") |> Enum.into(%{})"}
asts_reduce(asts \\ [])
asts_reduce(asts) :: ast

asts_reduce/1 takes one or more asts and reduces them to a single ast using Kernel.SpecialForms.unquote_splicing/1.

The list is first flattened and any nils removed before splicing.

An empty list reduces to nil.

Examples

iex> ast = quote(do: a = x + y)
...> [x: 42, y: 8, c: 5] |> helper_asts_reduce_eval(ast)
{50, "a = x + y"}

iex> ast = [quote(do: a = x + y),
...>        quote(do: a * c)]
...> [x: 42, y: 8, c: 5] |>  helper_asts_reduce_eval(ast)
{250, "(\n  a = x + y\n  a * c\n)"}

iex> ast = nil
...> [x: 42, y: 8, c: 5] |> helper_asts_reduce_eval(ast)
{nil, ""}

iex> ast = [
...>   quote(do: a = x + y),
...>   nil,
...>   [
...>    quote(do: b = a / c),
...>    nil,
...>    quote(do: d = b * b),
...>   ],
...>   quote(do: e = a + d),
...> ]
...> [x: 42, y: 8, c: 5] |>helper_asts_reduce_eval(ast)
{150.0, "(\n  a = x + y\n  b = a / c\n  d = b * b\n  e = a + d\n)"}
asts_sort(asts, weights \\ nil)
asts_sort(asts, asts_sort_weight_map | nil) :: asts

asts_sort/2 take one or more asts together with an (optional) weight map and returns a list of asts with the lower weight ast earlier in the list.

Examples

iex> quote(do: use X1) |> helper_asts_sort_to_string
["use(X1)"]

iex> nil |> helper_asts_sort_to_string
[]

iex> [
...>  quote(do: use X1),
...>  quote(do: require X2),
...>  quote(do: import X3),
...> ] |> helper_asts_sort_to_string
["require(X2)", "use(X1)", "import(X3)"]

iex> [
...>  quote(do: use X1),
...>  quote(do: require X2),
...>  quote(do: import X3),
...> ] |> helper_asts_sort_to_string(%{import: 1, use: 3})
["import(X3)", "use(X1)", "require(X2)"]

iex> [
...>  quote(do: use X1),
...>  quote(do: require X2),
...>  quote(do: import X3),
...> ] |> helper_asts_sort_to_string(%{import: 1, use: 3, default: 2})
["import(X3)", "require(X2)", "use(X1)"]
asts_sort_weight_default()
asts_sort_weight_default() :: any

asts_sort_weight_default/0 returns the hardcoded “backstop” weight:

Examples

iex> asts_sort_weight_default()
9999
asts_sort_weight_get(key, weights \\ %{alias: 2000, def: 5000, default: 9999, defdelegate: 2500, defmacro: 4000, import: 3000, require: 1000, use: 1500})
asts_sort_weight_get(asts_sort_weight_key, asts_sort_weight_map) :: any

asts_sort_weight_get/2 takes an ast “key” (the first element), and an (optional) weight map, and returns the weight.

If no weight map is supplied, the default map is used.

If the key does not exist in the weight map, the :default key is tried, else the “backstop” weight (asts_sort_weight_default/0) returned.

Examples

iex> :use |> asts_sort_weight_get
1500

iex> :require |> asts_sort_weight_get
1000

iex> :use |> asts_sort_weight_get(%{use: 42, require: 43, import: 44})
42

iex> :unknown |> asts_sort_weight_get(%{use: 42, require: 43, import: 44})
9999

iex> :unknown |> asts_sort_weight_get(%{use: 42, require: 43, import: 44, default: 99})
99
asts_sort_weights_default()
asts_sort_weights_default() :: map

asts_sort_weights_default/0 returns the default map used to sort asts.

asts_validate!(asts)
asts_validate!([ast]) :: [ast] | no_return

asts_validate!/1 validates a list of asts using ast_validate!/1, returns the asts or raises an ArgumentError exception on the first invalid one.

Examples

iex> [1, 2, 3] |> asts_validate!
[1, 2, 3]

iex> [1, {2, 2}, :three] |> asts_validate!
[1, {2, 2}, :three]

iex> [1, {2, 2, 2}, %{c: 3}] |> asts_validate!
** (ArgumentError) expected an ast, got: {:error, {2, 2, 2}}