plymio_ast v1.0.0 Plymio.Ast.Utility View Source
Utility Functions for Asts (Quoted Forms)
Documentation Terms
In the documentation there are terms, usually in italics, used to mean the same thing (e.g. form).
form
A form is an ast.
forms
A forms is zero, one or more forms.
form_index
A form_index value is a 2tuple where the first element is a form and the second an index (an integer).
Function Results
Many (new) functions either return {:ok, value} or {:error, error} where error will be as Exception.
The default action for bang functions when fielding an {:error, error} result is to raise the error.
Link to this section Summary
Functions
ast_enumerate/1 takes an ast and, if a :__block__, returns the list of args
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_prewalk/2 takes the same arguments as ast_postwalk/2 and works in an equivalen mannner to the latter
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 zero, 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_weight_get/2 takes an ast “key” (the first element), and
an (optional) weight map, and returns the weight
asts_sort_weights_default/0 returns the default map used to sort asts
form_enumerate/1 takes a form and, if a :__block__, returns {:ok, args} where args are the individual statements in the block
form_enumerate!/1 calls form_enumerate/1 and if the result is {:ok, forms} returns forms
form_index_normalise/2 take a value and an optional default_index and returns either {:ok, {form, index} or {:error, error}
form_index_normalise!/1 calls form_index_normalise/1 and if the result is {:ok, form_index} returns form_index
form_validate/1 calls Macro.validate/1 on the argument (the expected form)
and if the result is :ok returns {:ok, form}, else {:error, error}
form_validate!/1 calls form_validate/1 with the argument and if the
result is {:ok, form} returns the form
forms_pipe/1 takes a forms and uses Macro.pipe/3 to pipe them together and create a new form
forms_pipe!/1 calls forms_pipe/1 and if the result is {:ok, form} returns form
forms_reduce/1 takes a forms and reduces the forms to a single
form using Kernel.SpecialForms.unquote_splicing/1
forms_reduce!/1 calls forms_reduce/1 and if the result is {:ok, forms} returns forms
forms_validate/1 validates the forms using form_validate/1 on each form, returning {:ok, forms} if all are valid, else {:error, error}
forms_validate!/1 validates a forms using forms_validate/1
Link to this section Types
ast_fun_or_acc_fun_tuple() :: atom() | (ast() -> ast()) | {any(), atom()} | {any(), (ast(), any() -> {ast(), any()})}
asts_sort_weight_map() :: %{optional(asts_sort_weight_key()) => any()}
error() :: %ArgumentError{__exception__: term(), message: term()}
Link to this section Functions
ast_enumerate/1 takes an ast and, if a :__block__, returns the list of args.
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/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, []}
...> |> ast_from_mfa
...> |> helper_ast_test_forms_texts
{:ok, ["X1.f1()"]}
iex> {X1, :f1, [1, :two, "tre"]}
...> |> ast_from_mfa
...> |> helper_ast_test_forms_texts
{:ok, ["X1.f1(1, :two, \"tre\")"]}
iex> {X2, :f2, [{1, :two, "tre"}]}
...> |> ast_from_mfa
...> |> helper_ast_test_forms_texts
{:ok, ["X2.f2({1, :two, \"tre\"})"]}
iex> {X2, :f2, [{1, :two, "tre"} |> Macro.escape]}
...> |> ast_from_mfa
...> |> helper_ast_test_forms_texts
{:ok, ["X2.f2({1, :two, \"tre\"})"]}
iex> {X3, :f3, [%{a: 1, b: %{b: 2}, c: {3, :tre, "tre"}}]}
...> |> ast_from_mfa
...> |> helper_ast_test_forms_texts
{:ok, ["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]}
...> |> ast_from_mfa
...> |> helper_ast_test_forms_texts
{:ok, ["X3.f3(%{a: 1, b: %{b: 2}, c: {3, :tre, \"tre\"}})"]}
ast_postwalk(ast(), ast_fun_or_acc_fun_tuple()) :: ast_or_ast_acc_tuple()
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> quote do
...> x = x + 1
...> end
...> |> ast_postwalk(fn
...> {:x, _, _} -> Macro.var(:a, nil)
...> # passthru
...> x -> x
...> end)
...> |> helper_ast_test_forms_result_texts(binding: [a: 42])
{:ok, {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})
...> {:ok, result} = ast |> helper_ast_test_forms_result_texts(binding: [a: 42])
...> result |> Tuple.insert_at(0, acc) # add the accumulator
{7, 1844, ["(a = a + 1\n a = a * a\n a = a - 5)"]}
ast_prewalk(ast(), ast_fun_or_acc_fun_tuple()) :: ast_or_ast_acc_tuple()
ast_prewalk/2 takes the same arguments as ast_postwalk/2 and works in an equivalen mannner to the latter.
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/ 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.
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 |> rightast 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, afterMacro.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> [
...> Macro.var(:x, nil),
...> quote(do: fn x -> x * x end.()),
...> quote(do: List.wrap)
...> ]
...> |> asts_pipe
...> |> helper_ast_test_forms_result_texts(binding: [x: 42])
{:ok, {[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> [
...> 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
...> |> helper_ast_test_forms_result_texts(binding: [x: 42])
{:ok, {%{: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/1 takes zero, 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> quote(do: a = x + y)
...> |> asts_reduce
...> |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {50, ["a = x + y"]}}
iex> [quote(do: a = x + y),
...> quote(do: a * c)]
...> |> asts_reduce
...> |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {250, ["(a = x + y\n a * c)"]}}
iex> nil
...> |> asts_reduce
...> |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {nil, [""]}}
iex> [
...> quote(do: a = x + y),
...> nil,
...> [
...> quote(do: b = a / c),
...> nil,
...> quote(do: d = b * b),
...> ],
...> quote(do: e = a + d),
...> ]
...> |> asts_reduce
...> |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {150.0, ["(a = x + y\n b = a / c\n d = b * b\n e = a + d)"]}}
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/0 returns the hardcoded “backstop” weight:
Examples
iex> asts_sort_weight_default()
9999
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/0 returns the default map used to sort asts.
form_enumerate/1 takes a form and, if a :__block__, returns {:ok, args} where args are the individual statements in the block.
Examples
iex> 1 |> form_enumerate
{:ok, [1]}
iex> [1, 2, 3] |> form_enumerate
{:ok, [[1, 2, 3]]}
iex> :two |> form_enumerate
{:ok, [:two]}
iex> quote do
...> x = 1
...> y = 2
...> z = x + y
...> end
...> |> form_enumerate
{:ok, [quote(do: x = 1), quote(do: y = 2), quote(do: z = x + y)]}
iex> %{a: 1} |> form_enumerate
{:error, %ArgumentError{message: "expected a form; got: %{a: 1}"}}
form_enumerate!(any()) :: forms() | no_return()
form_enumerate!/1 calls form_enumerate/1 and if the result is {:ok, forms} returns forms.
Examples
iex> quote do
...> x = 1
...> y = 2
...> z = x + y
...> end
...> |> form_enumerate!
[quote(do: x = 1), quote(do: y = 2), quote(do: z = x + y)]
form_index_normalise/2 take a value and an optional default_index and returns either {:ok, {form, index} or {:error, error}.
If the value is already a valid form_index it is returned unchanged as {:ok, {form, index}.
If the value is a just a form, {:ok, {form, default_index}} is returned. The default default_index is 0 but can be overidden on the call..
Both form and index (or default_index) are validated (using Macro.validate/1, is_integer/1) and if either fails {:error, error} is returned.
Examples
iex> quote(do: a = x + y)
...> |> form_index_normalise
{:ok, {quote(do: a = x + y), 0}}
iex> quote(do: a = x + y)
...> |> form_index_normalise(-1)
{:ok, {quote(do: a = x + y), -1}}
iex> {:error, error} = %{a: 1}
...> |> form_index_normalise
...> match?(%ArgumentError{message: "expected a form; got: %{a: 1}"}, error)
true
iex> {:error, error} = {quote(do: a = x + y), :this_is_not_a_valid_index}
...> |> form_index_normalise
...> match?(%ArgumentError{message: "expected a valid index; got: :this_is_not_a_valid_index"}, error)
true
form_index_normalise!(any()) :: {form(), integer()} | no_return()
form_index_normalise!/1 calls form_index_normalise/1 and if the result is {:ok, form_index} returns form_index.
Examples
iex> quote(do: a = x + y)
...> |> form_index_normalise!
{quote(do: a = x + y), 0}
form_validate/1 calls Macro.validate/1 on the argument (the expected form)
and if the result is :ok returns {:ok, form}, else {:error, error}.
Examples
iex> 1 |> form_validate
{:ok, 1}
iex> nil |> form_validate # nil is a valid ast
{:ok, nil}
iex> [:x, :y] |> form_validate
{:ok, [:x, :y]}
iex> ast = {:x, :y} # this 2tuple is a valid ast without escaping
...> {:ok, result} = ast |> form_validate
...> ast |> helper_ast_compare(result)
{:ok, {:x, :y}}
iex> {:error, error} = {:x, :y, :z} |> form_validate
...> match?(%ArgumentError{message: "expected a form; got: {:x, :y, :z}"}, error)
true
iex> {:error, error} = %{a: 1, b: 2, c: 3} |> form_validate # map not a valid ast
...> match?(%ArgumentError{message: "expected a form; got: %{a: 1, b: 2, c: 3}"}, error)
true
iex> ast = %{a: 1, b: 2, c: 3} |> Macro.escape # escaped map is a valid ast
...> {:ok, result} = ast |> form_validate
...> ast |> helper_ast_compare(result)
{:ok, %{a: 1, b: 2, c: 3} |> Macro.escape}
form_validate!/1 calls form_validate/1 with the argument and if the
result is {:ok, form} returns the form.
Examples
iex> 1 |> form_validate!
1
iex> nil |> form_validate! # nil is a valid ast
nil
iex> [:x, :y] |> form_validate!
[:x, :y]
iex> ast = {:x, :y} # this 2tuple is a valid ast without escaping
...> result = ast |> form_validate!
...> ast |> helper_ast_compare!(result)
{:x, :y}
iex> {:x, :y, :z} |> form_validate!
** (ArgumentError) expected a form; got: {:x, :y, :z}
iex> %{a: 1, b: 2, c: 3} |> form_validate! # map not a valid ast
** (ArgumentError) expected a form; got: %{a: 1, b: 2, c: 3}
iex> ast = %{a: 1, b: 2, c: 3} |> Macro.escape # escaped map is a valid ast
...> result = ast |> form_validate!
...> ast |> helper_ast_compare!(result)
%{a: 1, b: 2, c: 3} |> Macro.escape
forms_pipe/1 takes a forms and uses Macro.pipe/3 to pipe them together and create a new form.
It returns {:ok, form} or {:error, error}.
Each form in the forms is passed to an Enum.reduce/3 function, together with the result (form) of all the pipe operations to date (i.e. the accumulator).
The default behaviour is for the latest form to become the zeroth
argument in the accumulator form (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 forms in the list can be a form_index. No index (i.e. just the form) implies index 0.
Any nil forms are ignored. An empty list returns {:ok, nil}.
Examples
No or empty arguments:
iex> forms_pipe()
{:ok, nil}
iex> [] |> forms_pipe()
{:ok, nil}
Simple arguments:
iex> 42 |> forms_pipe
{:ok, 42}
iex> :atom |> forms_pipe
{:ok, :atom}
An impossible pipe:
iex> {:ok, ast} = [42, :atom] |> forms_pipe
** (ArgumentError) cannot pipe 42 into :atom, can only pipe into local calls foo(), remote calls Foo.bar() or anonymous functions calls foo.()
This example show what happens when all the forms do not have an explicit index:
iex> {:ok, form} = [
...> Macro.var(:x, nil),
...> quote(do: fn x -> x * x end.()),
...> quote(do: List.wrap)
...> ]
...> |> forms_pipe
...> form |> helper_ast_test_forms_result_texts(binding: [x: 42])
{:ok, {[1764], ["List.wrap((fn x -> x * x end).(x))"]}}
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> {:ok, form} = [
...> 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(%{}))
...> ]
...> |> forms_pipe
...> form |> helper_ast_test_forms_result_texts(binding: [x: 42])
{:ok, {%{:p => :this_is_q, "y" => 42}, ["Enum.into((fn p, q, x, y -> [{y, x}, {p, q}] end).(:p, :this_is_q, x, \"y\"), %{})"]}}
forms_pipe!(any()) :: form() | no_return()
forms_pipe!/1 calls forms_pipe/1 and if the result is {:ok, form} returns form.
Examples
iex> form = [
...> Macro.var(:x, nil),
...> quote(do: fn x -> x * x end.()),
...> quote(do: List.wrap)
...> ]
...> |> forms_pipe!
...> form |> helper_ast_test_forms_result_texts(binding: [x: 42])
{:ok, {[1764], ["List.wrap((fn x -> x * x end).(x))"]}}
forms_reduce/1 takes a forms and reduces the forms to a single
form using Kernel.SpecialForms.unquote_splicing/1.
If the reduction suceeds, {:ok, reduced_form} is returned else {:error, error}.
The list is first flattened and any nils removed before splicing.
An empty list reduces to {:ok, nil}.
Examples
iex> {:ok, reduced_form} = quote(do: a = x + y) |> forms_reduce
...> reduced_form |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {50, ["a = x + y"]}}
iex> {:ok, reduced_form} = [
...> quote(do: a = x + y),
...> quote(do: a * c)
...> ] |> forms_reduce
...> reduced_form |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {250, ["(a = x + y\n a * c)"]}}
iex> nil
...> |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {nil, [""]}}
iex> {:ok, form} = [
...> quote(do: a = x + y),
...> nil,
...> [
...> quote(do: b = a / c),
...> nil,
...> quote(do: d = b * b),
...> ],
...> quote(do: e = a + d),
...> ] |> forms_reduce
...> form |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {150.0, ["(a = x + y\n b = a / c\n d = b * b\n e = a + d)"]}}
forms_reduce!(any()) :: form() | no_return()
forms_reduce!/1 calls forms_reduce/1 and if the result is {:ok, forms} returns forms.
Examples
iex> reduced_form = quote(do: a = x + y) |> forms_reduce!
...> reduced_form |> helper_ast_test_forms_result_texts(binding: [x: 42, y: 8, c: 5])
{:ok, {50, ["a = x + y"]}}
iex> [] |> forms_reduce!
nil
iex> nil |> forms_reduce!
nil
forms_validate/1 validates the forms using form_validate/1 on each form, returning {:ok, forms} if all are valid, else {:error, error}.
Examples
iex> [1, 2, 3] |> forms_validate
{:ok, [1, 2, 3]}
iex> [1, {2, 2}, :three] |> forms_validate
{:ok, [1, {2, 2}, :three]}
iex> {:error, error} = [1, {2, 2, 2}, %{c: 3}] |> forms_validate
...> match?(%ArgumentError{message: "expected valid forms; got invalid_indices: [1, 2]"}, error)
true
forms_validate!(list()) :: forms() | no_return()
forms_validate!/1 validates a forms using forms_validate/1.
If the result is {:ok, forms} returns the forms.
Examples
iex> [1, 2, 3] |> forms_validate!
[1, 2, 3]
iex> [1, {2, 2}, :three] |> forms_validate!
[1, {2, 2}, :three]
iex> [1, {2, 2, 2}, %{c: 3}] |> forms_validate!
** (ArgumentError) expected valid forms; got invalid_indices: [1, 2]