Quellcode durchsuchen

first commit

master
tmbb vor 5 Jahren
Commit
a408d3fd30
9 geänderte Dateien mit 554 neuen und 0 gelöschten Zeilen
  1. +4
    -0
      .formatter.exs
  2. +24
    -0
      .gitignore
  3. +21
    -0
      README.md
  4. +30
    -0
      config/config.exs
  5. +436
    -0
      lib/ex_to_erl.ex
  6. +27
    -0
      mix.exs
  7. +7
    -0
      mix.lock
  8. +4
    -0
      test/ex_to_erl_test.exs
  9. +1
    -0
      test/test_helper.exs

+ 4
- 0
.formatter.exs Datei anzeigen

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

+ 24
- 0
.gitignore Datei anzeigen

@ -0,0 +1,24 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
ex_to_erl-*.tar

+ 21
- 0
README.md Datei anzeigen

@ -0,0 +1,21 @@
# ExToErl
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ex_to_erl` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_to_erl, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/ex_to_erl](https://hexdocs.pm/ex_to_erl).

+ 30
- 0
config/config.exs Datei anzeigen

@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.
# You can configure your application as:
#
# config :ex_to_erl, key: :value
#
# and access this configuration in your application as:
#
# Application.get_env(:ex_to_erl, :key)
#
# You can also configure a 3rd-party app:
#
# config :logger, level: :info
#
# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
# import_config "#{Mix.env()}.exs"

+ 436
- 0
lib/ex_to_erl.ex Datei anzeigen

@ -0,0 +1,436 @@
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

+ 27
- 0
mix.exs Datei anzeigen

@ -0,0 +1,27 @@
defmodule ExToErl.MixProject do
use Mix.Project
def project do
[
app: :ex_to_erl,
version: "0.1.0",
elixir: "~> 1.7",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ex_doc, "~> 0.21.1", only: [:dev]}
]
end
end

+ 7
- 0
mix.lock Datei anzeigen

@ -0,0 +1,7 @@
%{
"earmark": {:hex, :earmark, "1.3.3", "5e8be428fcef362692b6dbd7dc55bdc7023da26d995cb3fb19aa4bd682bfd3f9", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.21.1", "5ac36660846967cd869255f4426467a11672fec3d8db602c429425ce5b613b90", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
}

+ 4
- 0
test/ex_to_erl_test.exs Datei anzeigen

@ -0,0 +1,4 @@
defmodule ExToErlTest do
use ExUnit.Case
doctest ExToErl
end

+ 1
- 0
test/test_helper.exs Datei anzeigen

@ -0,0 +1 @@
ExUnit.start()

Laden…
Abbrechen
Speichern