|
|
@ -0,0 +1,436 @@ |
|
|
|
defmodule ExToErl do |
|
|
|
@moduledoc """ |
|
|
|
Utilities to convert Elixir expressions into the corresponding Erlang. |
|
|
|
|
|
|
|
This package is meant to be used as a learning tool or as part of development workflow. |
|
|
|
It was written to answer questions like: "What does this Elixir expression compile to?". |
|
|
|
It's very useful to explore the output of the Elixir compiler in a user-friendly way. |
|
|
|
|
|
|
|
One should be careful when using this in production with user supplied input |
|
|
|
because most functions in this module run the Elixir compiler and generate atoms |
|
|
|
dynamically at runtime (as the Elixir compiler does). |
|
|
|
|
|
|
|
The code might also be victim of race conditions (I haven't tested running it in parallel, though). |
|
|
|
|
|
|
|
It has no tests yet, but I hope it will have some in the future. |
|
|
|
The API will probably change a lot. |
|
|
|
I might switch from raising errors to returning `{:ok, value}` and `:error`. |
|
|
|
""" |
|
|
|
|
|
|
|
@sandbox_module ExToEarl.Sandboxes.ElixirExpressionCompilerSandbox |
|
|
|
|
|
|
|
@doc """ |
|
|
|
Extracts the Erlang abstract code from a BEAM module. |
|
|
|
|
|
|
|
The argument to this function can be either: |
|
|
|
|
|
|
|
- The module name (an atom) |
|
|
|
- A `{:module, module, binary, _}` tuple, returned by `Module.create/3` |
|
|
|
- The `binary` part from the tuple above |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
TODO |
|
|
|
""" |
|
|
|
def beam_to_erlang_abstract_code(module) do |
|
|
|
beam = |
|
|
|
case module do |
|
|
|
module when is_atom(module) -> |
|
|
|
:code.which(module) |
|
|
|
|
|
|
|
{:module, _, binary, _} when is_binary(binary) -> |
|
|
|
binary |
|
|
|
end |
|
|
|
|
|
|
|
{:ok, {_, [{:abstract_code, {_, abstract_code}}]}} = :beam_lib.chunks(beam, [:abstract_code]) |
|
|
|
|
|
|
|
abstract_code |
|
|
|
end |
|
|
|
|
|
|
|
@doc """ |
|
|
|
Extracts the Erlang abstract code from a BEAM module and converts it |
|
|
|
into Erlang source code. |
|
|
|
|
|
|
|
The argument to this function can be either: |
|
|
|
|
|
|
|
- The module name (an atom) |
|
|
|
- A `{:module, module, binary, _}` tuple, returned by `Module.create/3` |
|
|
|
- The `binary` part from the tuple above |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
iex> module = Module.create(MyModule, quote(do: def f(x) do x end), __ENV__) |
|
|
|
{:module, MyModule, |
|
|
|
<<70, 79, 82, 49, 0, 0, 3, 220, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 124, |
|
|
|
0, 0, 0, 13, 15, 69, 108, 105, 120, 105, 114, 46, 77, 121, 77, 111, 100, 117, |
|
|
|
108, 101, 8, 95, 95, 105, 110, 102, 111, ...>>, {:f, 1}} |
|
|
|
|
|
|
|
iex> ExToErl.beam_to_erlang_abstract_code(module) |
|
|
|
[ |
|
|
|
{:attribute, 6, :file, {'iex', 6}}, |
|
|
|
{:attribute, 6, :module, MyModule}, |
|
|
|
{:attribute, 6, :compile, [:no_auto_import]}, |
|
|
|
{:attribute, 6, :export, [__info__: 1, f: 1]}, |
|
|
|
{:attribute, 6, :spec, |
|
|
|
{{:__info__, 1}, |
|
|
|
[ |
|
|
|
{:type, 6, :fun, |
|
|
|
[ |
|
|
|
{:type, 6, :product, |
|
|
|
[ |
|
|
|
{:type, 6, :union, |
|
|
|
[ |
|
|
|
{:atom, 6, :attributes}, |
|
|
|
{:atom, 6, :compile}, |
|
|
|
{:atom, 6, :functions}, |
|
|
|
{:atom, 6, :macros}, |
|
|
|
{:atom, 6, :md5}, |
|
|
|
{:atom, 6, :module}, |
|
|
|
{:atom, 6, :deprecated} |
|
|
|
]} |
|
|
|
]}, |
|
|
|
{:type, 6, :any, []} |
|
|
|
]} |
|
|
|
]}}, |
|
|
|
{:function, 0, :__info__, 1, |
|
|
|
[ |
|
|
|
{:clause, 0, [{:atom, 0, :module}], [], [{:atom, 0, MyModule}]}, |
|
|
|
{:clause, 0, [{:atom, 0, :functions}], [], |
|
|
|
[{:cons, 0, {:tuple, 0, [{:atom, 0, :f}, {:integer, 0, 1}]}, {nil, 0}}]}, |
|
|
|
{:clause, 0, [{:atom, 0, :macros}], [], [nil: 0]}, |
|
|
|
{:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :attributes}}], [], |
|
|
|
[ |
|
|
|
{:call, 0, |
|
|
|
{:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}}, |
|
|
|
[{:atom, 0, MyModule}, {:var, 0, :Key}]} |
|
|
|
]}, |
|
|
|
{:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :compile}}], [], |
|
|
|
[ |
|
|
|
{:call, 0, |
|
|
|
{:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}}, |
|
|
|
[{:atom, 0, MyModule}, {:var, 0, :Key}]} |
|
|
|
]}, |
|
|
|
{:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :md5}}], [], |
|
|
|
[ |
|
|
|
{:call, 0, |
|
|
|
{:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}}, |
|
|
|
[{:atom, 0, MyModule}, {:var, 0, :Key}]} |
|
|
|
]}, |
|
|
|
{:clause, 0, [{:atom, 0, :deprecated}], [], [nil: 0]} |
|
|
|
]}, |
|
|
|
{:function, 6, :f, 1, |
|
|
|
[{:clause, 6, [{:var, 6, :__@1}], [], [{:var, 6, :__@1}]}]} |
|
|
|
] |
|
|
|
""" |
|
|
|
def beam_to_erlang_source(module) do |
|
|
|
abstract_code = beam_to_erlang_abstract_code(module) |
|
|
|
erlang_abstract_code_to_string(:erl_syntax.form_list(abstract_code)) |
|
|
|
end |
|
|
|
|
|
|
|
@doc """ |
|
|
|
Extracts the Erlang abstract code from a BEAM module, converts it |
|
|
|
into Erlang source code and writes it into a file. |
|
|
|
|
|
|
|
The first argument to this function can be either: |
|
|
|
|
|
|
|
- The module name (an atom) |
|
|
|
- A `{:module, module, binary, _}` tuple, returned by `Module.create/3` |
|
|
|
- The `binary` part from the tuple above |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
iex> module = Module.create(MyModule, quote(do: def f(x) do x end), __ENV__) |
|
|
|
{:module, MyModule, |
|
|
|
<<70, 79, 82, 49, 0, 0, 3, 220, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 124, |
|
|
|
0, 0, 0, 13, 15, 69, 108, 105, 120, 105, 114, 46, 77, 121, 77, 111, 100, 117, |
|
|
|
108, 101, 8, 95, 95, 105, 110, 102, 111, ...>>, {:f, 1}} |
|
|
|
|
|
|
|
iex> ExToErl.beam_to_erlang_source(module) |> IO.puts() |
|
|
|
-file("iex", 3). |
|
|
|
|
|
|
|
-module('Elixir.MyModule'). |
|
|
|
|
|
|
|
-compile([no_auto_import]). |
|
|
|
|
|
|
|
-export(['__info__'/1, f/1]). |
|
|
|
|
|
|
|
-spec '__info__'(attributes | compile | functions | |
|
|
|
macros | md5 | module | deprecated) -> any(). |
|
|
|
|
|
|
|
'__info__'(module) -> 'Elixir.MyModule'; |
|
|
|
'__info__'(functions) -> [{f, 1}]; |
|
|
|
'__info__'(macros) -> []; |
|
|
|
'__info__'(Key = attributes) -> |
|
|
|
erlang:get_module_info('Elixir.MyModule', Key); |
|
|
|
'__info__'(Key = compile) -> |
|
|
|
erlang:get_module_info('Elixir.MyModule', Key); |
|
|
|
'__info__'(Key = md5) -> |
|
|
|
erlang:get_module_info('Elixir.MyModule', Key); |
|
|
|
'__info__'(deprecated) -> []. |
|
|
|
|
|
|
|
f(__@1) -> __@1. |
|
|
|
|
|
|
|
:ok |
|
|
|
""" |
|
|
|
def beam_to_erlang_source(module, filename) do |
|
|
|
contents = beam_to_erlang_source(module) |
|
|
|
File.write(filename, contents) |
|
|
|
end |
|
|
|
|
|
|
|
@doc """ |
|
|
|
Converts a string containing Elixir code into an Erlang expression. |
|
|
|
|
|
|
|
This function expects an Elixir expression. |
|
|
|
If you supply a block (which is a valid Elixir expression), only the last one |
|
|
|
will be converted into an Erlang expression. |
|
|
|
This limitation is a result of the fact that in Erlang a sequence of instructions |
|
|
|
if not a an Erlang expression (on the other hand, a sequence of Elixir |
|
|
|
expressions is an Elixir expression). |
|
|
|
Don't use this function to convert entire Elixir modules to Erlang. |
|
|
|
Use `ExToErl.beam_to_erlang_source/1` instead. |
|
|
|
|
|
|
|
The function raises if the string is not valid Elixir. |
|
|
|
As with most functions in this module, this function *creates atoms at runtime* |
|
|
|
because valid Erlang AST contains atoms. |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
Single expressions: |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_abstract_code("a + b") |
|
|
|
{:op, 1, :+, {:var, 1, :_a@1}, {:var, 1, :_b@1}} |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_abstract_code("a <= b") |
|
|
|
{:op, 1, :"=<", {:var, 1, :_a@1}, {:var, 1, :_b@1}} |
|
|
|
|
|
|
|
Elixir blocks (only the last expression is returned): |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_abstract_code("_ = a + b; c + d") |
|
|
|
{:op, 1, :+, {:var, 1, :_c@1}, {:var, 1, :_d@1}} |
|
|
|
|
|
|
|
You can import functions and macros inside your Elixir expression: |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_abstract_code("import Bitwise; a >>> b") |
|
|
|
{:op, 1, :bsr, {:var, 1, :_a@1}, {:var, 1, :_b@1}} |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_abstract_code("import Bitwise; a &&& b") |
|
|
|
{:op, 1, :band, {:var, 1, :_a@1}, {:var, 1, :_b@1}} |
|
|
|
|
|
|
|
Some expressions may raise warnings, although they should be the same wanings |
|
|
|
as if the Elixir expression were to be compiled inside a normal Elixir module: |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_abstract_code("a = b") |
|
|
|
warning: variable "a" is unused |
|
|
|
|
|
|
|
warning: variable "a" is unused |
|
|
|
|
|
|
|
{:match, 1, {:var, 1, :_a@2}, {:var, 1, :_b@1}} |
|
|
|
|
|
|
|
Some Elixir operators are actually macros or special forms which can be expanded |
|
|
|
into quite complex Erlang code: |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_abstract_code("a or b") |
|
|
|
{:case, 1, {:var, 1, :_a@1}, |
|
|
|
[ |
|
|
|
{:clause, [generated: true, location: 1], [{:atom, 0, false}], [], |
|
|
|
[{:var, 1, :_b@1}]}, |
|
|
|
{:clause, [generated: true, location: 1], [{:atom, 0, true}], [], |
|
|
|
[{:atom, 0, true}]}, |
|
|
|
{:clause, [generated: true, location: 1], [{:var, 1, :__@1}], [], |
|
|
|
[ |
|
|
|
{:call, 1, {:remote, 1, {:atom, 0, :erlang}, {:atom, 1, :error}}, |
|
|
|
[{:tuple, 1, [{:atom, 0, :badbool}, {:atom, 0, :or}, {:var, 1, :__@1}]}]} |
|
|
|
]} |
|
|
|
]} |
|
|
|
|
|
|
|
""" |
|
|
|
def elixir_source_to_erlang_abstract_code(elixir) do |
|
|
|
ast = Code.string_to_quoted!(elixir) |
|
|
|
elixir_ast_to_erlang_abstract_code(ast) |
|
|
|
end |
|
|
|
|
|
|
|
@doc ~S""" |
|
|
|
Converts a string containing Elixir code into Erlang source code. |
|
|
|
|
|
|
|
This function expects an Elixir expression. |
|
|
|
If you supply a block (which is a valid Elixir expression), only the last one |
|
|
|
will be converted into an Erlang expression. |
|
|
|
This limitation is a result of the fact that in Erlang a sequence of instructions |
|
|
|
if not a an Erlang expression (on the other hand, a sequence of Elixir expressions |
|
|
|
is an Elixir expression). |
|
|
|
Don't use this function to convert entire Elixir modules to Erlang source code. |
|
|
|
Use `ExToErl.beam_to_erlang_source/1` instead. |
|
|
|
|
|
|
|
The function raises if the string is not valid Elixir. |
|
|
|
As with most functions in this module, this function *creates atoms at runtime* |
|
|
|
because valid Erlang AST contains atoms. |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_source("a") |
|
|
|
"_a@1\n" |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_source("a + b") |
|
|
|
"_a@1 + _b@1\n" |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_source("a + b < f.(x)") |
|
|
|
"_a@1 + _b@1 < _f@1(_x@1)\n" |
|
|
|
|
|
|
|
iex> ExToErl.elixir_source_to_erlang_source("a or b") |> IO.puts() |
|
|
|
case _a@1 of |
|
|
|
false -> _b@1; |
|
|
|
true -> true; |
|
|
|
__@1 -> erlang:error({badbool, 'or', __@1}) |
|
|
|
end |
|
|
|
|
|
|
|
:ok |
|
|
|
|
|
|
|
iex(3)> ExToErl.elixir_source_to_erlang_source("a.b") |> IO.puts() |
|
|
|
case _a@1 of |
|
|
|
#{b := __@1} -> __@1; |
|
|
|
__@1 when erlang:is_map(__@1) -> |
|
|
|
erlang:error({badkey, b, __@1}); |
|
|
|
__@1 -> __@1:b() |
|
|
|
end |
|
|
|
|
|
|
|
:ok |
|
|
|
""" |
|
|
|
def elixir_source_to_erlang_source(elixir) do |
|
|
|
abstract_code = elixir_source_to_erlang_abstract_code(elixir) |
|
|
|
erlang_abstract_code_to_string(abstract_code) |
|
|
|
end |
|
|
|
|
|
|
|
@doc """ |
|
|
|
Converts Elixir AST into Erlang abstract code. |
|
|
|
|
|
|
|
This function expects an Elixir expression. |
|
|
|
If you supply a block (which is a valid Elixir expression), only the last one |
|
|
|
will be converted into an Erlang expression. |
|
|
|
This limitation is a result of the fact that in Erlang a sequence of instructions |
|
|
|
if not a an Erlang expression (on the other hand, a sequence of Elixir expressions |
|
|
|
is an Elixir expression). |
|
|
|
|
|
|
|
As with most functions in this module, this function *creates atoms at runtime* |
|
|
|
because valid Erlang AST contains atoms. |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
iex> ExToErl.elixir_ast_to_erlang_abstract_code({:+, [line: 1], [{:a, [line: 1], nil}, {:b, [line: 1], nil}]}) |
|
|
|
{:op, 1, :+, {:var, 1, :_a@1}, {:var, 1, :_b@1}} |
|
|
|
|
|
|
|
iex> Code.string_to_quoted!("a - b") |> ExToErl.elixir_ast_to_erlang_abstract_code() |
|
|
|
{:op, 1, :-, {:var, 1, :_a@1}, {:var, 1, :_b@1}} |
|
|
|
""" |
|
|
|
def elixir_ast_to_erlang_abstract_code(ast) do |
|
|
|
variables = extract_variables_from_elixir_ast(ast) |
|
|
|
|
|
|
|
module_body = |
|
|
|
quote do |
|
|
|
@moduledoc "Just a temporary place to store some Erlang abstract code" |
|
|
|
def main(unquote_splicing(variables)) do |
|
|
|
unquote(ast) |
|
|
|
end |
|
|
|
end |
|
|
|
|
|
|
|
{:module, module_name, _, _} = module = Module.create(@sandbox_module, module_body, __ENV__) |
|
|
|
|
|
|
|
full_module_abstract_code = beam_to_erlang_abstract_code(module) |
|
|
|
function = find_function_by_name(full_module_abstract_code, :main) |
|
|
|
body = extract_body_from_function_clause(function) |
|
|
|
|
|
|
|
# Delete the module to avoid the annoying warning about redefining modules. |
|
|
|
# Because functions in this module will never be called, there's no need to purge the module. |
|
|
|
:code.purge(module_name) |
|
|
|
true = :code.delete(module_name) |
|
|
|
|
|
|
|
body |
|
|
|
end |
|
|
|
|
|
|
|
@doc """ |
|
|
|
Parses an Erlang expression into erlang abstract code. |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
iex> ExToErl.erlang_source_to_abstract_code("A + B.") |
|
|
|
{:op, 1, :+, {:var, 1, :A}, {:var, 1, :B}} |
|
|
|
|
|
|
|
iex> ExToErl.erlang_source_to_abstract_code("A < B.") |
|
|
|
{:op, 1, :<, {:var, 1, :A}, {:var, 1, :B}} |
|
|
|
|
|
|
|
iex> ExToErl.erlang_source_to_abstract_code("A + B * C < F + G.") |
|
|
|
{:op, 1, :<, |
|
|
|
{:op, 1, :+, {:var, 1, :A}, {:op, 1, :*, {:var, 1, :B}, {:var, 1, :C}}}, |
|
|
|
{:op, 1, :+, {:var, 1, :F}, {:var, 1, :G}}} |
|
|
|
|
|
|
|
iex> ExToErl.erlang_source_to_abstract_code("A + B * C < f(x) + g(y).") |
|
|
|
{:op, 1, :<, |
|
|
|
{:op, 1, :+, {:var, 1, :A}, {:op, 1, :*, {:var, 1, :B}, {:var, 1, :C}}}, |
|
|
|
{:op, 1, :+, {:call, 1, {:atom, 1, :f}, [{:atom, 1, :x}]}, |
|
|
|
{:call, 1, {:atom, 1, :g}, [{:atom, 1, :y}]}}} |
|
|
|
|
|
|
|
iex(9)> ExToErl.erlang_source_to_abstract_code("A + B * C < f(X) + g(Y).") |
|
|
|
{:op, 1, :<, |
|
|
|
{:op, 1, :+, {:var, 1, :A}, {:op, 1, :*, {:var, 1, :B}, {:var, 1, :C}}}, |
|
|
|
{:op, 1, :+, {:call, 1, {:atom, 1, :f}, [{:var, 1, :X}]}, |
|
|
|
{:call, 1, {:atom, 1, :g}, [{:var, 1, :Y}]}}} |
|
|
|
|
|
|
|
""" |
|
|
|
def erlang_source_to_abstract_code(bin) do |
|
|
|
charlist = String.to_charlist(bin) |
|
|
|
{:ok, tokens, _} = :erl_scan.string(charlist) |
|
|
|
{:ok, [expression]} = :erl_parse.parse_exprs(tokens) |
|
|
|
expression |
|
|
|
end |
|
|
|
|
|
|
|
@doc """ |
|
|
|
Pretty prints Erlang abstract code as Erlang source code. |
|
|
|
|
|
|
|
## Examples |
|
|
|
|
|
|
|
TODO |
|
|
|
""" |
|
|
|
def erlang_abstract_code_to_string(abstract_code, opts \\ []) do |
|
|
|
indent = Keyword.get(opts, :indent, 8) |
|
|
|
|
|
|
|
[:erl_prettypr.format(abstract_code), "\n"] |
|
|
|
|> to_string() |
|
|
|
|> String.replace("\t", String.duplicate(" ", indent)) |
|
|
|
end |
|
|
|
|
|
|
|
# ---------------------------------- |
|
|
|
# Private functions |
|
|
|
# ---------------------------------- |
|
|
|
|
|
|
|
defp find_function_by_name(forms, name) do |
|
|
|
Enum.find(forms, fn form -> |
|
|
|
case form do |
|
|
|
{:function, _line, ^name, _arity, clauses} when is_list(clauses) -> |
|
|
|
true |
|
|
|
|
|
|
|
_ -> |
|
|
|
false |
|
|
|
end |
|
|
|
end) |
|
|
|
end |
|
|
|
|
|
|
|
# Extracts the list of variables form an Elixir AST fragment |
|
|
|
defp extract_variables_from_elixir_ast(ast) do |
|
|
|
{_ast, variables} = |
|
|
|
Macro.postwalk(ast, [], fn ast_node, variables -> |
|
|
|
case ast_node do |
|
|
|
{name, _meta, module} = variable when is_atom(name) and is_atom(module) -> |
|
|
|
{ast_node, [variable | variables]} |
|
|
|
|
|
|
|
_other -> |
|
|
|
{ast_node, variables} |
|
|
|
end |
|
|
|
end) |
|
|
|
|
|
|
|
variables |
|
|
|
end |
|
|
|
|
|
|
|
defp extract_body_from_function_clause({:function, _line, _name, _arity, [clause]}) do |
|
|
|
{:clause, _line, _args, _guards, body} = clause |
|
|
|
List.last(body) |
|
|
|
end |
|
|
|
end |