Docs here: https://hexdocs.pm/ex_to_erl/ExToErl.html (with lots of examples!)
Github repo here: https://github.com/tmbb/ex_to_erl
While working on my (still mostly vaporware) mutation testing framework, I found myself working with Erlang abstract code generated by the Elixir compiler. It’s useful to have short snippets for testing purposes, and it turns out it’s very hard to convert an Elixir expression into the Erlang equivalent. It’s actually very easy to get the abstract code of a full module compiled by the Elixir compiler but there is no easy way of getting the result of a single expression such as a + b
.
I’ve decided to isolate the functionality that does the conversion into its own package so that others might be able to play with it, and in the process learn a bit more about the code the Elixir compile emits. This is meant to be used as a learning tool and not as part of a serious application that runs in production.
Examples
Convert Elixir source into erlang source:
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
Convert elixir source into erlang abstract code:
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}]}]}
]}
]}
For more examples, read the docs in the link above.
Implementation details
This package works by doing scary things in the BEAM runtime. First, it generates a dummy Elixir module with a single function in it; inside that function it places the elixir expression(s). Then it compiles the code into an in-memory BEAM file. Then, it extracts the erlang abstract code from the BEAM file, and if needed converts it into source code using functions from the Erlang standard library.
Because it compiels Elixir code, it generates atoms at runtime. It’s possible that if you try to convert expressions concurrently there will be race conditions (I have to fix it by making everything go through a pool of genservers so that no two processes try to compile the same module at the same time; I have no idea which protections are in place to avoid this happening…)