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
|