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