Packages

plymio_ast_vorm: Managing a Collection of Quoted Forms (Asts)

Current section

Files

Jump to
plymio_ast_vorm lib ast vorm vorm.ex
Raw

lib/ast/vorm/vorm.ex

defmodule Plymio.Ast.Vorm do
@moduledoc ~S"""
The module provides a *toolkit* for Managing a Collection of Quoted Forms (Asts).
Forms can be added, deleted, inserted, replaced, transformed, compiled, etc.
## Documentation Terms
In the documentation below these terms, usually in *italics*, are used to mean the same thing (e.g. *vorm forms*).
### *form*
A valid ast.
### *forms*
One or more (`List`) of *form*s.
### *vorm*
An instance of the module's state (create by e.g. `new/1`)
### *vorm forms*
The *forms* held in the *vorm*.
### *form_range*
The *forms* targetted by a function (e.g. `filter/2`).
The *form_range* is normalised to a *predicate* function that, when passed a `{form, index}` tuple, returns either `true` or `false`.
Here is a selection of valid form ranges. Note an `index` is zero-based and negative values are allowed.
| Form Range | Forms Targetted |
| :--- | :--- |
| 0 | *first form* |
| -1 | *last form* |
| [2, 0] | *first and third forms* |
| %{1 => :a, 3 => nil} | *second and fourth forms* |
| nil | *all forms* |
| `predicate_function` | when `true` |
### *new_forms*
*new_forms* is used to refer to the new *forms* to add, insert, replace, etc.
### *form_index_transform*
A pipeline of one or more arity one functions
Each function in the pipeline will be passed a `{:form,index}` tuple and should return either `{:ok, form}` or `{:error, error}`. Any other results is converted to `{:ok, form}`. An error aborts the pipeline. The returned form is used create the `{transformed_form,index}` tuple to feed the next function in the pipeline.
### *form_walk_transform*
A pipeline of one or more arity one functions
A *form_walk_function* is an arity one function suitable as the second argument to `Macro.postwalk/2` or `Macro.prewalk/2`.
Each *form* will be passed through sucessively each function in the *form_walk_transform* pipeline.
### *forms transform*
A pipeline of one or more arity one functions
Each function in the pipeline will be passed the current *forms* and should return `{:ok, forms}` or `{:error, error}`. Any other results is converted to `{:ok, forms}`. The returned *forms* will be used to feed the next function in the pipeline. An error aborts the pipeline.
## Function Results
Unless otherwise stated, the result of the function will be either `{:ok, vorm}` or `{:error, error}` where `error` is an `Exception`.
## Vorm Struct
The module's `struct` holds its state and has only one field:
| Field | Purpose |
| :--- | :--- |
| `forms` | *the forms collection* |
"""
import Kernel, except: [length: 1]
alias Plymio.Ast.Utility, as: PAU
alias Plymio.Ast.Vorm.Utility, as: PATU
alias Plymio.Ast.Vorm.Utility.Pipeline, as: PAVUP
use Plymio.Ast.Vorm.Attribute
@type opts :: Keyword.t
@type t :: %__MODULE__{}
@type kv :: {any,any}
@type error :: struct
@type form :: Macro.t
@type forms :: [form]
@type items :: list
import Plymio.Ast.Vorm.Vormen, only: [
vormen_create_transform: 3,
vormen_create_transform: 4,
vormen_validate: 1,
]
import Plymio.Ast.Vorm.Vormen.Transform, only: [
normalise_form_index_transform: 1,
normalise_form_postwalk_transform: 1,
normalise_form_prewalk_transform: 1,
normalise_form_traverse_transform: 3,
normalise_vormen_transform: 1,
]
@doc false
defdelegate forms_pipe(asts), to: PAU
@doc false
defdelegate forms_reduce(form), to: PAU
@doc false
defdelegate form_validate(form), to: PAU
@pav_form_defstruct [
{@pav_key_forms, @pav_value_forms_initial_value},
]
@pav_form_struct_kvs_aliases [
@pav_alias_forms,
]
@pav_form_struct_dict_aliases @pav_form_struct_kvs_aliases
|> PATU.opts_create_aliases_dict
@doc false
def opts_canon_keys!(opts, dict \\ @pav_form_struct_dict_aliases) do
opts |> PATU.opts_canon_keys!(dict)
end
defstruct @pav_form_defstruct
@doc ~S"""
`new/1` is used to create a new *vorm*
## Examples
iex> {:ok, vorm} = new()
...> vorm |> helper_vorm_test_forms!
{nil, []}
The forms can be populated at the same time:
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{15, ["x = x + 1", "x = x * x", "x = x - 1"]}
"""
@spec new(opts) :: {:ok, t} | {:error, error}
def new(opts \\ [])
def new([]) do
{:ok, %__MODULE__{}}
end
def new(opts) do
with {:ok, %__MODULE__{} = vorm} <- new() do
vorm |> update(opts)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`new!/1` calls `new/1` and if the result is `{:ok, vorm}` returns the `vorm`. Otherwise the `error` in `{:error, error}` is raised.
## Examples
iex> vorm = new!()
...> vorm |> helper_vorm_test_forms!
{nil, []}
The forms can be populated at the same time:
iex> vorm = new!(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{15, ["x = x + 1", "x = x * x", "x = x - 1"]}
"""
@spec new!(opts) :: t | no_return
def new!(opts \\ []) do
opts
|> new()
|> case do
{:ok, %__MODULE__{} = state} -> state
{:error, error} -> raise error
end
end
@spec update_field(t, kv) :: {:ok, t} | {:error, error}
defp update_field(state, kv)
defp update_field(%__MODULE__{} = state, {k,new_forms})
when k in [
@pav_key_forms,
] do
new_forms
|> case do
@pav_value_forms_initial_value ->
{:ok, state |> struct!([{@pav_key_forms, @pav_value_forms_initial_value}])}
new_forms ->
with {:ok, new_forms} <- new_forms |> vormen_validate do
{:ok, state |> struct!([{@pav_key_forms, new_forms}])}
else
{:error, _} = result -> result
end
end
end
@spec update(t, opts) :: {:ok, t} | {:error, error}
defp update(vorm, opts)
defp update(%__MODULE__{} = state, []) do
{:ok, state}
end
defp update(%__MODULE__{} = state, opts) when is_list(opts) do
opts
|> opts_canon_keys!
|> Enum.reduce_while(state, fn {k,v}, s ->
s
|> update_field({k,v})
|> case do
%__MODULE__{} = s -> {:cont, s}
{:ok, %__MODULE__{} = s} -> {:cont, s}
{:error, error} -> {:halt, error}
end
end)
|> case do
{:error, _} = result -> result
%__MODULE__{} = vorm -> {:ok, vorm}
end
end
@doc false
@spec vorm_update_forms(t, any) :: {:ok, t} | {:error, error}
def vorm_update_forms(%__MODULE__{} = state, new_forms) do
state |> update([{@pav_key_forms, new_forms}])
end
@doc false
@spec express(t) :: {:ok, {forms, t}} | {:error, error}
def express(%__MODULE__{@pav_key_forms => forms} = state) do
{:ok, {forms, state}}
end
@doc false
@spec produce(t) :: {:ok, {forms, t}} | {:error, error}
@spec produce(opts) :: {:ok, {forms, t}} | {:error, error}
def produce(vorm)
def produce(%__MODULE__{} = state) do
with {:ok, {_forms, %__MODULE__{}}} = result <- state |> express do
result
else
{:error, _} = result -> result
end
end
def produce(opts) when is_list(opts) do
with {:ok, %__MODULE__{} = vorm} <- opts |> new(),
{:ok, {_forms, %__MODULE__{}}} = result <- vorm |> produce do
result
else
{:error, _} = result -> result
end
end
@doc false
defmacro reify(opts \\ []) do
quote bind_quoted: [
opts: opts
] do
with {:ok, {forms, %Plymio.Ast.Vorm{}}} <- opts |> Plymio.Ast.Vorm.produce() do
forms
|> Code.eval_quoted([], __ENV__)
else
{:error, error} -> raise error
end
end
end
@doc ~S"""
`produce_pipeline/2` take a list (often a `Keyword`) of `{verb,args}` tuples and an (optional) *vorm*.
If the *vorm* is `nil`, a new, empty one is created.
Each `verb` must be the name of one of the functions that return `{:ok, vorm}` on sucess (e.g. `add`).
Each `{verb, args}` tuple is applied to the *vorm* (using `Enum.reduce/3`).
The result on sucess will be `{:ok, {forms, vorm}}`
Each `verb` has aliases:
| Canononical Verb | Verb Aliases |
| :---------------- | -------------------: |
| `:add` | *:add_forms, :add_form* |
| `:filter` | *:filter_forms, :filter_form* |
| `:insert` | *:insert_forms, :insert_form* |
| `:pipe_before` | *:pipe_before_forms, :pipe_before_form* |
| `:pipe_after` | *:pipe_after_forms, :pipe_after_form* |
| `:postwalk` | *:postwalk_forms, :postwalk_form* |
| `:prewalk` | *:prewalk_forms, :prewalk_form* |
| `:put` | *:put_forms, :put_form* |
| `:reject` | *:reject_forms, :reject_form* |
| `:reduce` | *:reduce_forms, :reduce_form* |
| `:replace` | *:replace_forms, :replace_form* |
| `:transform` | *:transform_forms, :transform_form* |
| `:traverse` | *:traverse_forms, :traverse_form* |
## Examples
These initial two examples demonstrate the *vorm* being created automatically if not given as the second argument.
iex> {:ok, {_forms, vorm}} = produce_pipeline(add: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
iex> {:ok, {_forms, vorm}} = produce_pipeline(
...> add: quote(do: x = x + 1),
...> add_forms: [quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
This example uses an existing *vorm*
iex> {:ok, vorm} = new(form: quote(do: x = x + 1))
...> {:ok, {_forms, vorm}} = produce_pipeline(
...> [add_forms: [quote(do: x = x * x), quote(do: x = x - 1)]],
...> vorm)
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
Combining different actions (e.g. `:add`, `:insert`, etc.
iex> {:ok, {_forms, vorm}} = produce_pipeline(
...> add_form: quote(do: x = x * x),
...> insert_form: [0, quote(do: x = x + 1)],
...> insert: [:append, quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
iex> {:ok, {_forms, vorm}} = produce_pipeline(
...> add: quote(do: x = x * x),
...> insert: [0, quote(do: x = x + 1)],
...> insert: [:append, quote(do: x = x - 1)],
...> postwalk: [nil,
...> fn
...> {:x, _, m} when is_atom(m) -> quote(do: y)
...> x -> x
...> end])
...> vorm |> helper_vorm_test_forms!(binding: [y: 11])
{143, ["y = y + 1", "y = y * y", "y = y - 1"]}
iex> {:ok, {_forms, vorm}} = produce_pipeline(add: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)],
...> put: quote(do: x = x * x * x))
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{27, ["x = x * x * x"]}
Values that are not 2tuples will be intepreted as an `add`.
iex> {:ok, {_forms, vorm}} = produce_pipeline([
...> quote(do: x = x + 1),
...> quote(do: x = x * x),
...> quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
Using mix *form* and explicit `{verb,args}` syntax:
iex> {:ok, {_forms, vorm}} = produce_pipeline([
...> quote(do: x = x + 1),
...> {:add, [quote(do: x = x * x), quote(do: x = x - 1)]},
...> {:insert_form, [:append, quote(do: x = x * x * x)]}])
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{3375, ["x = x + 1", "x = x * x", "x = x - 1", "x = x * x * x"]}
Using mixed *form* and `Keyword` `verb: args` syntax:
iex> {:ok, {_forms, vorm}} = produce_pipeline([
...> quote(do: x = x + 1),
...> add: [quote(do: x = x * x), quote(do: x = x - 1)],
...> insert: [:append, quote(do: x = x * x * x)]])
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{3375, ["x = x + 1", "x = x * x", "x = x - 1", "x = x * x * x"]}
iex> {:ok, {_forms, vorm}} = produce_pipeline([
...> quote(do: x = x + 1),
...> [add_forms: [quote(do: x = x * x), quote(do: x = x - 1)]],
...> [insert: [:append, quote(do: x = x * x * x)]]])
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{3375, ["x = x + 1", "x = x * x", "x = x - 1", "x = x * x * x"]}
The verb here (`:add_typo`) is not known and a `KeyError` is returned:
iex> {:error, %KeyError{} = error} = produce_pipeline(add_typo: quote(do: x = x + 1))
...> error.key
:add_typo
"""
@spec produce_pipeline(any, any) :: {:ok, {forms, t}} | {:error, error}
def produce_pipeline(opts, vorm \\ nil)
def produce_pipeline(opts, nil) when is_list(opts) do
with {:ok, vorm} <- new() do
opts |> produce_pipeline(vorm)
else
{:error, _} = result -> result
end
end
def produce_pipeline([], %__MODULE__{} = state) do
{:ok, state}
end
def produce_pipeline(opts, %__MODULE__{} = state) when is_list(opts) do
with {:ok, %__MODULE__{} = state} <- state |> PAVUP.pipeline(opts),
{:ok, {_forms, %__MODULE__{}}} = result <- state |> express do
result
else
{:error, _} = result -> result
end
end
def produce_pipeline(opts, new_opts) when is_list(opts) and is_list(new_opts) do
with {:ok, %__MODULE__{} = state} <- new_opts |> new,
{:ok, {_forms, %__MODULE__{}}} = result <- opts |> produce_pipeline(state) do
result
else
{:error, _} = result -> result
end
end
@doc ~S"""
`reify_pipeline/2` is a macro that calls `produce_pipeline/2` and compiles the `forms` from the sucess result `{:ok, {forms, vorm}}`.
"""
defmacro reify_pipeline(pipeline, opts \\ []) do
quote bind_quoted: [
pipeline: pipeline,
opts: opts
] do
with {:ok, {forms, %Plymio.Ast.Vorm{}}} <- pipeline |> Plymio.Ast.Vorm.produce_pipeline(opts) do
forms
|> Code.eval_quoted([], __ENV__)
else
{:error, error} -> raise error
end
end
end
### want collate to pull form_range of forms, unquote splicign and put to 2dn form_range; default append.
@doc ~S"""
`reduce/2` take a *vorm* and a *forms transform* and passes the *vorm forms* to the normalised *forms transform*, expecting `{:ok, reduced_forms}` as the result.
The `reduced_forms` are used to update the *vorm forms*.
## Examples
The indentity reduction does nothing:
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, collate_vorm} = vorm |> reduce(&(&1))
...> collate_vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
This example uses `Plymio.Ast.Utility.forms_reduce/1` to create a single *form* from the *vorm forms*.
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, collate_vorm} = vorm |> reduce(&Plymio.Ast.Utility.forms_reduce/1)
...> collate_vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["(x = x + 1\n x = x * x\n x = x - 1)"]}
"""
@spec reduce(any, any) :: {:ok, t} | {:error, error}
def reduce(vorm, form_reduce)
def reduce(%__MODULE__{@pav_key_forms => forms} = state, reduce) do
with {:ok, fun_reduce} <- reduce |> normalise_vormen_transform do
forms
|> fun_reduce.()
|> case do
{:error, _} = result -> result
{:ok, _} = result -> result
forms -> {:ok, forms}
end
|> case do
{:error, _} = result -> result
{:ok, forms} -> state |> vorm_update_forms(forms)
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`transform/2` take a *vorm*, *form_range* and a *form_index_transform*.
The normalised *form_index_transform* is applied to each *form* in the *vorm forms*.
## Examples
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> transform(
...> 1, fn {_form,_index} -> quote(do: x = x * x * x) end)
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{63, ["x = x + 1", "x = x * x * x", "x = x - 1"]}
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> transform(
...> nil, fn {_form,index} -> quote(do: y = y + unquote(index)) end)
...> vorm |> helper_vorm_test_forms!(binding: [y: 5])
{8, ["y = y + 0", "y = y + 1", "y = y + 2"]}
"""
@spec transform(any, any, any) :: {:ok, t} | {:error, error}
def transform(vorm, form_range, form_transform \\ [])
def transform(state, _form_range, []) do
{:ok, state}
end
def transform(%__MODULE__{@pav_key_forms => forms} = state, form_range, transform) do
with {:ok, fun_transform} <- transform |> normalise_form_index_transform do
fun_then = fn fi, _new_forms, forms ->
fi
|> fun_transform.()
|> case do
{:error, _} = result -> result
{:ok, new_form} -> forms ++ [new_form]
new_form -> forms ++ [new_form]
end
end
fun_else = fn {form,_index}, _new_forms, forms ->
{:ok, forms ++ [form]}
end
transform_opts = [
{@pav_key_fun_then, fun_then},
{@pav_key_fun_else, fun_else},
]
with {:ok, new_forms} <- forms |> vormen_create_transform(form_range,
@pav_transform_type_transform, transform_opts) do
state |> vorm_update_forms(new_forms)
else
{:error, _} = result -> result
end
else
{:error, _} = result -> result
end
end
@doc ~S"""
`postwalk/2` take a *vorm*, *form_range* and *form_walk_transform*.
The *form_walk_transform* is applied to each *form* in the *form_range* using `Macro.postwalk/2`.
## Examples
iex> {:ok, vorm} = new(form: quote(do: z = x + y))
...> {:ok, vorm} = vorm |> postwalk(0, fn
...> {:x, _, m} when is_atom(m) -> quote(do: a)
...> {:y, _, m} when is_atom(m) -> quote(do: b)
...> x -> x
...> end)
...> vorm |> helper_vorm_test_forms!(binding: [a: 39, b: 3])
{42, ["z = a + b"]}
Here two transforms are applied:
iex> {:ok, vorm} = new(form: quote(do: z = x + y))
...> {:ok, vorm} = vorm |> postwalk(0, [
...> fn
...> {:x, _, m} when is_atom(m) -> quote(do: a)
...> {:y, _, m} when is_atom(m) -> quote(do: b)
...> x -> x
...> end,
...> fn
...> {:a, _, m} when is_atom(m) -> quote(do: p)
...> {:b, _, m} when is_atom(m) -> quote(do: q)
...> x -> x
...> end])
...> vorm |> helper_vorm_test_forms!(binding: [p: 20, q: 22])
{42, ["z = p + q"]}
"""
@spec postwalk(any, any, any) :: {:ok, t} | {:error, error}
def postwalk(vorm, form_range, form_postwalk \\ [])
def postwalk(state, _form_range, []) do
{:ok, state}
end
def postwalk(%__MODULE__{} = state, form_range, postwalk) do
with {:ok, fun_postwalk} <- postwalk |> normalise_form_postwalk_transform,
{:ok, %__MODULE__{}} = result <- state |> transform(form_range, fun_postwalk) do
result
else
{:error, _} = result -> result
end
end
@doc ~S"""
`prewalk/2` take a *vorm*, *form_range* and *form_walk_transform*.
The *form_walk_transform* is applied to each *form* in the *form_range* using `Macro.prewalk/2`.
## Examples
iex> {:ok, vorm} = new(form: quote(do: z = x + y))
...> {:ok, vorm} = vorm |> prewalk(0, fn
...> {:x, _, m} when is_atom(m) -> quote(do: a)
...> {:y, _, m} when is_atom(m) -> quote(do: b)
...> x -> x
...> end)
...> vorm |> helper_vorm_test_forms!(binding: [a: 39, b: 3])
{42, ["z = a + b"]}
Here two transforms are applied:
iex> {:ok, vorm} = new(form: quote(do: z = x + y))
...> {:ok, vorm} = vorm |> prewalk(0, [
...> fn
...> {:x, _, m} when is_atom(m) -> quote(do: a)
...> {:y, _, m} when is_atom(m) -> quote(do: b)
...> x -> x
...> end,
...> fn
...> {:a, _, m} when is_atom(m) -> quote(do: p)
...> {:b, _, m} when is_atom(m) -> quote(do: q)
...> x -> x
...> end])
...> vorm |> helper_vorm_test_forms!(binding: [p: 20, q: 22])
{42, ["z = p + q"]}
"""
@spec prewalk(any, any, any) :: {:ok, t} | {:error, error}
def prewalk(vorm, form_range, form_prewalk \\ [])
def prewalk(state, _form_range, []) do
{:ok, state}
end
def prewalk(%__MODULE__{} = state, form_range, prewalk) do
with {:ok, fun_prewalk} <- prewalk |> normalise_form_prewalk_transform,
{:ok, %__MODULE__{}} = result <- state |> transform(form_range, fun_prewalk) do
result
else
{:error, _} = result -> result
end
end
@doc ~S"""
`traverse/2` take a *vorm*, *form_range*, the initial traverse accummulator, the **pre** *form_traverse_transform* and the **post** *form_traverse_transform*.
`Macro.traverse/4` returns `{new_form, acc}`; `new_form` is used to replace the original one and `acc` is ignored.
## Examples
This contrived example uses the *pre* stage to change the `x` and `y` vars to `a` and `b`, and the *post* stage to change `value1` and `value2` to their values in the accumulator (a lookup dictionary).
iex> {:ok, vorm} = new(form: quote do
...> x = value1
...> y = value2
...> z = x + y
...> end)
...> {:ok, vorm} = vorm |> traverse(-1,
...> # the accumulator is a lookup dictionary
...> %{value1: 39, value2: 3},
...> # pre function
...> fn
...> # x to a
...> {:x, _, _}, acc -> {Macro.var(:a,nil), acc}
...> # y to b
...> {:y, _, _}, acc -> {Macro.var(:b,nil), acc}
...> # passthru
...> ast, acc -> {ast, acc}
...> end,
...> # post function
...> fn
...> # is the var (form) in the acc lookup table? if so replace with value
...> {form, _, mod} = ast, acc when is_atom(form) and is_atom(mod) ->
...> case acc |> Map.has_key?(form) do
...> true -> {Map.get(acc,form), acc}
...> _ -> {ast, acc}
...> end
...> # passthru
...> ast, acc -> {ast, acc}
...> end)
...> vorm |> helper_vorm_test_forms!(binding: [])
{42, ["(a = 39\n b = 3\n z = a + b)"]}
"""
@spec traverse(any, any, any, any, any) :: {:ok, t} | {:error, error}
def traverse(vorm, form_range, acc_traverse, pre_traverse, post_traverse)
def traverse(%__MODULE__{} = state, form_range, acc_traverse, pre_traverse, post_traverse) do
with {:ok, traverse_transform} <- normalise_form_traverse_transform(acc_traverse, pre_traverse, post_traverse),
{:ok, %__MODULE__{}} = result <- state |> transform(form_range, traverse_transform) do
result
else
{:error, _} = result -> result
end
end
@doc ~S"""
`length/1` returns the number of *vorm forms*.
## Examples
iex> {:ok, vorm} = new()
...> vorm |> Plymio.Ast.Vorm.length
0
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> Plymio.Ast.Vorm.length
3
"""
@spec length(t) :: integer
def length(vorm)
def length(%__MODULE__{@pav_key_forms => forms}) do
forms |> Kernel.length
end
@doc ~S"""
`empty?/1` takes a *vorm* and returns `true` if the *vorm forms* is empty, else `false`.
## Examples
iex> {:ok, vorm} = new()
...> vorm |> Plymio.Ast.Vorm.empty?
true
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> Plymio.Ast.Vorm.empty?
false
"""
@spec empty?(t) :: true | false
def empty?(vorm)
def empty?(%__MODULE__{@pav_key_forms => forms}) do
case forms |> Kernel.length do
0 -> true
_ -> false
end
end
@doc ~S"""
`add/2` appends *new_forms* to the *vorm forms*.
## Examples
iex> {:ok, vorm} = new!() |> add(quote(do: x = x + 1))
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{8, ["x = x + 1"]}
iex> {:ok, vorm} = new!() |> add(quote(do: x = x + 1))
...> {:ok, vorm} = vorm |> add([quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
iex> {:ok, vorm} = new!() |> add([
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{15, ["x = x + 1", "x = x * x", "x = x - 1"]}
iex> {:ok, vorm} = new!() |> add(nil)
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{nil, []}
iex> {:ok, vorm} = new!() |> add([])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{nil, []}
"""
@spec add(t, any) :: {:ok, t} | {:error, error}
def add(vorm, new_forms)
def add(%__MODULE__{@pav_key_forms => forms} = state, new_forms) do
with {:ok, new_forms} <- new_forms |> vormen_validate do
state |> vorm_update_forms(forms ++ new_forms)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`put/2` completely replaces the *vorm forms* with *new_forms*
## Examples
iex> {:ok, vorm} = new(form: (quote(do: x = x + 1)))
...> {:ok, vorm} = vorm |> put([quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{48, ["x = x * x", "x = x - 1"]}
iex> {:ok, vorm} = new!() |> put([
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{15, ["x = x + 1", "x = x * x", "x = x - 1"]}
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, put_vorm} = vorm |> put(quote(do: x = 42))
...> put_vorm |> helper_vorm_test_forms!(binding: [x: 7])
{42, ["x = 42"]}
"""
@spec put(t, any) :: {:ok, t} | {:error, error}
def put(vorm, new_forms)
def put(%__MODULE__{} = state, new_forms) do
with {:ok, forms} <- new_forms |> vormen_validate do
state |> vorm_update_forms(forms)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`fetch/3` take a *vorm* and *form_range* and returns the forms in the *form_range* as `{:ok, forms}`.
The default *form_range* is `nil` i.e. return all forms.
## Examples
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, forms} = vorm |> fetch(1)
...> {:ok, fetch_vorm} = new(forms: forms)
...> fetch_vorm |> helper_vorm_test_forms!(binding: [x: 7])
{49, ["x = x * x"]}
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, forms} = vorm |> fetch([0,-1])
...> {:ok, fetch_vorm} = new(forms: forms)
...> fetch_vorm |> helper_vorm_test_forms!(binding: [x: 7])
{7, ["x = x + 1", "x = x - 1"]}
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, forms} = vorm |> fetch
...> {:ok, fetch_vorm} = new(forms: forms)
...> fetch_vorm |> helper_vorm_test_forms!(binding: [x: 7])
{63, ["x = x + 1", "x = x * x", "x = x - 1"]}
"""
@spec fetch(t, any) :: {:ok, forms} | {:error, error}
def fetch(vorm, form_range \\ nil)
def fetch(%__MODULE__{@pav_key_forms => forms}, form_range) do
with {:ok, _forms} = result <- forms |> vormen_create_transform(form_range, @pav_transform_type_fetch) do
result
else
{:error, _} = result -> result
end
end
@doc ~S"""
`filter/2` takes a *vorm* and *form_range* and filters *only* those forms in the *form_range*
## Examples
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> filter([-1,0])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{7, ["x = x + 1", "x = x - 1"]}
"""
@spec filter(t, any) :: {:ok, t} | {:error, error}
def filter(vorm, form_range)
def filter(%__MODULE__{@pav_key_forms => forms} = state, form_range) do
with {:ok, new_forms} <- forms |> vormen_create_transform(form_range, @pav_transform_type_filter) do
state |> vorm_update_forms(new_forms)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`reject/2` takes a *vorm* and *form_range* and deletes all forms in the *form_range*.
## Examples
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> reject([0,2])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{49, ["x = x * x"]}
"""
@spec reject(t, any) :: {:ok, t} | {:error, error}
def reject(vorm, form_range)
def reject(%__MODULE__{@pav_key_forms => forms} = state, form_range) do
with {:ok, new_forms} <- forms |> vormen_create_transform(form_range, @pav_transform_type_reject) do
state |> vorm_update_forms(new_forms)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`insert/3` take a *vorm*, *form_range* and *new_forms*.
The *new_forms* are *spliced* into the *vorm forms* at **each** index in the *form_range*.
As a special case, if the *form_range* is `:append`, the *new_forms* are *appended* (i.e. `add/2`)
## Examples
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> insert(2, quote(do: x = x * x * x))
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{262143, ["x = x + 1", "x = x * x", "x = x * x * x", "x = x - 1"]}
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> insert(nil, quote(do: x = x + 42))
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{8505, ["x = x + 42", "x = x + 1", "x = x + 42", "x = x * x", "x = x + 42", "x = x - 1"]}
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> insert(:append, quote(do: x = x + 42))
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{105, ["x = x + 1", "x = x * x", "x = x - 1", "x = x + 42"]}
Inserting into an empty *vorm* with *form_range* `nil` is same as `:append`
iex> {:ok, vorm} = new()
...> {:ok, vorm} = vorm |> insert(nil, quote(do: x = x * x * x))
...> vorm |> helper_vorm_test_forms!(binding: [x: 3])
{27, ["x = x * x * x"]}
"""
@spec insert(t, any, any) :: {:ok, t} | {:error, error}
def insert(vorm, form_range, new_forms)
def insert(%__MODULE__{} = state, form_range, new_forms)
when form_range in [@pav_key_append] do
state |> add(new_forms)
end
def insert(%__MODULE__{@pav_key_forms => forms} = state, nil, new_forms)
when Kernel.length(forms) == 0 do
state |> insert(@pav_key_append, new_forms)
end
def insert(%__MODULE__{@pav_key_forms => forms} = state, form_range, new_forms) do
with {:ok, new_forms} <- new_forms |> vormen_validate,
{:ok, forms} <- forms
|> vormen_create_transform(form_range, @pav_transform_type_insert, new_forms: new_forms) do
state |> vorm_update_forms(forms)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`replace/3` take a *vorm*, *form_range* and *new_forms*.
The *new_forms* are *spliced* into the *vorm forms* at **each** index in the *form_range*, replacing the existing *form*.
## Examples
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> replace(1, quote(do: x = x * x * x))
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{511, ["x = x + 1", "x = x * x * x", "x = x - 1"]}
iex> {:ok, vorm} = new(forms: [
...> quote(do: x = x + 1), quote(do: x = x * x), quote(do: x = x - 1)])
...> {:ok, vorm} = vorm |> replace([0,-1], quote(do: x = x * x * x))
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{1628413597910449, ["x = x * x * x", "x = x * x", "x = x * x * x"]}
"""
@spec replace(t, any, any) :: {:ok, t} | {:error, error}
def replace(vorm, form_range, new_forms)
def replace(%__MODULE__{@pav_key_forms => forms} = state, form_range, new_forms) do
with {:ok, new_forms} <- new_forms |> vormen_validate,
{:ok, new_forms} <- forms
|> vormen_create_transform(form_range, @pav_transform_type_replace, new_forms: new_forms) do
state |> vorm_update_forms(new_forms)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`pipe_before/2` take a *vorm*, *form_range* and *new_forms*.
Each *form* in the *form_range* is piped *before* the *new_forms*.
This example pipes a var (`x`) into a call to an anonymous function that adds 1:
iex> {:ok, vorm} = new(forms: quote(do: x))
...> {:ok, vorm} = vorm |> pipe_before(0, quote(do: fn v -> v + 1 end.()))
...> vorm |> helper_vorm_test_forms!(binding: [x: 42])
{43, ["(fn v -> v + 1 end).(x)"]}
This example selects the last (`-`) form to be before a list of forms.
iex> 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.())
...> {:ok, vorm} = new(forms: [quote(do: x = x + 5), quote(do: x)])
...> {:ok, vorm} = vorm |> pipe_before(-1, [before_ast1, before_ast2, before_ast3])
...> vorm |> helper_vorm_test_forms!(binding: [x: 37])
{-1848, ["x = x + 5", "(fn v -> 1 - v end).((fn v -> v * v end).((fn v -> v + 1 end).(x)))"]}
Implicit in the examples above has been that the left side form of
the pipe (`|>`) should become the zeroth argument of the right side
form. However the call to `Macro.pipe/3` that does the piping takes
the index (zero offset integer) 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.
> Note the *pipe* index is unrelated to the *form* 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`).
iex> before_ast = quote(do: Kernel.-(100))
...> {:ok, vorm} = new(form: quote(do: x))
...> {:ok, vorm} = vorm |> pipe_before(-1, before_ast)
...> vorm |> helper_vorm_test_forms!(binding: [x: 42])
{-58, ["Kernel.-(x, 100)"]}
iex> before_ast = {quote(do: Kernel.-(100)), 1}
...> {:ok, vorm} = new(form: quote(do: x))
...> {:ok, vorm} = vorm |> pipe_before(-1, before_ast)
...> vorm |> helper_vorm_test_forms!(binding: [x: 42])
{58, ["Kernel.-(100, x)"]}
This somewhat complicated example demonstrates using multiple asts with indicies:
iex> {:ok, vorm} = new(form: Macro.var(:x, nil))
...> {:ok, vorm} = vorm |> pipe_before(-1, [
...> {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},
...> ])
...> vorm |> helper_vorm_test_forms!(binding: [x: 7])
{144.0, ["Enum.reduce([&Enum.sum/1, fn v -> v * v end], Kernel.++([1, 2, 3], List.wrap(Kernel./(42, x))), fn f, s -> f.(s) end)"]}
"""
@spec pipe_before(t, any, any) :: {:ok, t} | {:error, error}
def pipe_before(vorm, form_range, new_forms)
def pipe_before(%__MODULE__{@pav_key_forms => forms} = state, form_range, new_forms) do
with {:ok, new_forms} <- new_forms |> vormen_validate,
{:ok, new_forms} <- forms
|> vormen_create_transform(form_range, @pav_transform_type_pipe_before, new_forms: new_forms) do
state |> vorm_update_forms(new_forms)
else
{:error, _} = result -> result
end
end
@doc ~S"""
`pipe_after/2` take a *vorm*, *form_range* and *new_forms*.
Each *form* in the *form_range* is piped *after* the *new_forms*.
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> {:ok, vorm} = new(form: quote(do: fn v -> v + 1 end.()))
...> {:ok, vorm} = vorm |> pipe_after(0, quote(do: x))
...> vorm |> helper_vorm_test_forms!(binding: [x: 42])
{43, ["(fn v -> v + 1 end).(x)"]}
Again, the same example as for `pipe_before` that supplies a list of asts to the
but with the arguments switched around:
iex> after_ast1 = Macro.var(:x, nil)
...> after_ast2 = quote(do: fn v -> v + 1 end.())
...> after_ast3 = quote(do: fn v -> v * v end.())
...> {:ok, vorm} = new(form: quote(do: fn v -> 1 - v end.()))
...> {:ok, vorm} = vorm |> pipe_after(-1, [after_ast1, after_ast2, after_ast3])
...> vorm |> helper_vorm_test_forms!(binding: [x: 42])
{-1848, ["(fn v -> 1 - v end).((fn v -> v * v end).((fn v -> v + 1 end).(x)))"]}
"""
@spec pipe_after(t, any, any) :: {:ok, t} | {:error, error}
def pipe_after(vorm, form_range, new_forms)
def pipe_after(%__MODULE__{@pav_key_forms => forms} = state, form_range, new_forms) do
with {:ok, new_forms} <- new_forms |> vormen_validate,
{:ok, new_forms} <- forms
|> vormen_create_transform(form_range, @pav_transform_type_pipe_after, new_forms: new_forms) do
state |> vorm_update_forms(new_forms)
else
{:error, _} = result -> result
end
end
end
defimpl Inspect, for: Plymio.Ast.Vorm do
use Plymio.Ast.Vorm.Attribute
def inspect(%Plymio.Ast.Vorm{@pav_key_forms => forms}, _opts) do
forms_telltale =
case forms do
x when is_nil(x) -> "X"
x when is_list(x) -> "#{inspect length(x)}"
end
"PAV(#{forms_telltale})"
end
end