ex_to_erl v0.1.0 ExToErl

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.

Link to this section Summary

Functions

Extracts the Erlang abstract code from a BEAM module.

Extracts the Erlang abstract code from a BEAM module and converts it into Erlang source code.

Extracts the Erlang abstract code from a BEAM module, converts it into Erlang source code and writes it into a file.

Converts Elixir AST into Erlang abstract code.

Converts a string containing Elixir code into an Erlang expression.

Converts a string containing Elixir code into Erlang source code.

Pretty prints Erlang abstract code as Erlang source code.

Parses an Erlang expression into erlang abstract code.

Link to this section Functions

Link to this function

beam_to_erlang_abstract_code(module)

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

Link to this function

beam_to_erlang_source(module)

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}]}]}
]
Link to this function

beam_to_erlang_source(module, filename)

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
Link to this function

elixir_ast_to_erlang_abstract_code(ast)

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}}
Link to this function

elixir_source_to_erlang_abstract_code(elixir)

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}]}]}
    ]}
]}
Link to this function

elixir_source_to_erlang_source(elixir)

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
Link to this function

erlang_abstract_code_to_string(abstract_code, opts \\ [])

Pretty prints Erlang abstract code as Erlang source code.

Examples

TODO

Link to this function

erlang_source_to_abstract_code(bin)

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}]}}}