You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

436 lines
15 KiB

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