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

5 years ago
  1. defmodule ExToErl do
  2. @moduledoc """
  3. Utilities to convert Elixir expressions into the corresponding Erlang.
  4. This package is meant to be used as a learning tool or as part of development workflow.
  5. It was written to answer questions like: "What does this Elixir expression compile to?".
  6. It's very useful to explore the output of the Elixir compiler in a user-friendly way.
  7. One should be careful when using this in production with user supplied input
  8. because most functions in this module run the Elixir compiler and generate atoms
  9. dynamically at runtime (as the Elixir compiler does).
  10. The code might also be victim of race conditions (I haven't tested running it in parallel, though).
  11. It has no tests yet, but I hope it will have some in the future.
  12. The API will probably change a lot.
  13. I might switch from raising errors to returning `{:ok, value}` and `:error`.
  14. """
  15. @sandbox_module ExToEarl.Sandboxes.ElixirExpressionCompilerSandbox
  16. @doc """
  17. Extracts the Erlang abstract code from a BEAM module.
  18. The argument to this function can be either:
  19. - The module name (an atom)
  20. - A `{:module, module, binary, _}` tuple, returned by `Module.create/3`
  21. - The `binary` part from the tuple above
  22. ## Examples
  23. TODO
  24. """
  25. def beam_to_erlang_abstract_code(module) do
  26. beam =
  27. case module do
  28. module when is_atom(module) ->
  29. :code.which(module)
  30. {:module, _, binary, _} when is_binary(binary) ->
  31. binary
  32. end
  33. {:ok, {_, [{:abstract_code, {_, abstract_code}}]}} = :beam_lib.chunks(beam, [:abstract_code])
  34. abstract_code
  35. end
  36. @doc """
  37. Extracts the Erlang abstract code from a BEAM module and converts it
  38. into Erlang source code.
  39. The argument to this function can be either:
  40. - The module name (an atom)
  41. - A `{:module, module, binary, _}` tuple, returned by `Module.create/3`
  42. - The `binary` part from the tuple above
  43. ## Examples
  44. iex> module = Module.create(MyModule, quote(do: def f(x) do x end), __ENV__)
  45. {:module, MyModule,
  46. <<70, 79, 82, 49, 0, 0, 3, 220, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 124,
  47. 0, 0, 0, 13, 15, 69, 108, 105, 120, 105, 114, 46, 77, 121, 77, 111, 100, 117,
  48. 108, 101, 8, 95, 95, 105, 110, 102, 111, ...>>, {:f, 1}}
  49. iex> ExToErl.beam_to_erlang_abstract_code(module)
  50. [
  51. {:attribute, 6, :file, {'iex', 6}},
  52. {:attribute, 6, :module, MyModule},
  53. {:attribute, 6, :compile, [:no_auto_import]},
  54. {:attribute, 6, :export, [__info__: 1, f: 1]},
  55. {:attribute, 6, :spec,
  56. {{:__info__, 1},
  57. [
  58. {:type, 6, :fun,
  59. [
  60. {:type, 6, :product,
  61. [
  62. {:type, 6, :union,
  63. [
  64. {:atom, 6, :attributes},
  65. {:atom, 6, :compile},
  66. {:atom, 6, :functions},
  67. {:atom, 6, :macros},
  68. {:atom, 6, :md5},
  69. {:atom, 6, :module},
  70. {:atom, 6, :deprecated}
  71. ]}
  72. ]},
  73. {:type, 6, :any, []}
  74. ]}
  75. ]}},
  76. {:function, 0, :__info__, 1,
  77. [
  78. {:clause, 0, [{:atom, 0, :module}], [], [{:atom, 0, MyModule}]},
  79. {:clause, 0, [{:atom, 0, :functions}], [],
  80. [{:cons, 0, {:tuple, 0, [{:atom, 0, :f}, {:integer, 0, 1}]}, {nil, 0}}]},
  81. {:clause, 0, [{:atom, 0, :macros}], [], [nil: 0]},
  82. {:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :attributes}}], [],
  83. [
  84. {:call, 0,
  85. {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
  86. [{:atom, 0, MyModule}, {:var, 0, :Key}]}
  87. ]},
  88. {:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :compile}}], [],
  89. [
  90. {:call, 0,
  91. {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
  92. [{:atom, 0, MyModule}, {:var, 0, :Key}]}
  93. ]},
  94. {:clause, 0, [{:match, 0, {:var, 0, :Key}, {:atom, 0, :md5}}], [],
  95. [
  96. {:call, 0,
  97. {:remote, 0, {:atom, 0, :erlang}, {:atom, 0, :get_module_info}},
  98. [{:atom, 0, MyModule}, {:var, 0, :Key}]}
  99. ]},
  100. {:clause, 0, [{:atom, 0, :deprecated}], [], [nil: 0]}
  101. ]},
  102. {:function, 6, :f, 1,
  103. [{:clause, 6, [{:var, 6, :__@1}], [], [{:var, 6, :__@1}]}]}
  104. ]
  105. """
  106. def beam_to_erlang_source(module) do
  107. abstract_code = beam_to_erlang_abstract_code(module)
  108. erlang_abstract_code_to_string(:erl_syntax.form_list(abstract_code))
  109. end
  110. @doc """
  111. Extracts the Erlang abstract code from a BEAM module, converts it
  112. into Erlang source code and writes it into a file.
  113. The first argument to this function can be either:
  114. - The module name (an atom)
  115. - A `{:module, module, binary, _}` tuple, returned by `Module.create/3`
  116. - The `binary` part from the tuple above
  117. ## Examples
  118. iex> module = Module.create(MyModule, quote(do: def f(x) do x end), __ENV__)
  119. {:module, MyModule,
  120. <<70, 79, 82, 49, 0, 0, 3, 220, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 124,
  121. 0, 0, 0, 13, 15, 69, 108, 105, 120, 105, 114, 46, 77, 121, 77, 111, 100, 117,
  122. 108, 101, 8, 95, 95, 105, 110, 102, 111, ...>>, {:f, 1}}
  123. iex> ExToErl.beam_to_erlang_source(module) |> IO.puts()
  124. -file("iex", 3).
  125. -module('Elixir.MyModule').
  126. -compile([no_auto_import]).
  127. -export(['__info__'/1, f/1]).
  128. -spec '__info__'(attributes | compile | functions |
  129. macros | md5 | module | deprecated) -> any().
  130. '__info__'(module) -> 'Elixir.MyModule';
  131. '__info__'(functions) -> [{f, 1}];
  132. '__info__'(macros) -> [];
  133. '__info__'(Key = attributes) ->
  134. erlang:get_module_info('Elixir.MyModule', Key);
  135. '__info__'(Key = compile) ->
  136. erlang:get_module_info('Elixir.MyModule', Key);
  137. '__info__'(Key = md5) ->
  138. erlang:get_module_info('Elixir.MyModule', Key);
  139. '__info__'(deprecated) -> [].
  140. f(__@1) -> __@1.
  141. :ok
  142. """
  143. def beam_to_erlang_source(module, filename) do
  144. contents = beam_to_erlang_source(module)
  145. File.write(filename, contents)
  146. end
  147. @doc """
  148. Converts a string containing Elixir code into an Erlang expression.
  149. This function expects an Elixir expression.
  150. If you supply a block (which is a valid Elixir expression), only the last one
  151. will be converted into an Erlang expression.
  152. This limitation is a result of the fact that in Erlang a sequence of instructions
  153. if not a an Erlang expression (on the other hand, a sequence of Elixir
  154. expressions is an Elixir expression).
  155. Don't use this function to convert entire Elixir modules to Erlang.
  156. Use `ExToErl.beam_to_erlang_source/1` instead.
  157. The function raises if the string is not valid Elixir.
  158. As with most functions in this module, this function *creates atoms at runtime*
  159. because valid Erlang AST contains atoms.
  160. ## Examples
  161. Single expressions:
  162. iex> ExToErl.elixir_source_to_erlang_abstract_code("a + b")
  163. {:op, 1, :+, {:var, 1, :_a@1}, {:var, 1, :_b@1}}
  164. iex> ExToErl.elixir_source_to_erlang_abstract_code("a <= b")
  165. {:op, 1, :"=<", {:var, 1, :_a@1}, {:var, 1, :_b@1}}
  166. Elixir blocks (only the last expression is returned):
  167. iex> ExToErl.elixir_source_to_erlang_abstract_code("_ = a + b; c + d")
  168. {:op, 1, :+, {:var, 1, :_c@1}, {:var, 1, :_d@1}}
  169. You can import functions and macros inside your Elixir expression:
  170. iex> ExToErl.elixir_source_to_erlang_abstract_code("import Bitwise; a >>> b")
  171. {:op, 1, :bsr, {:var, 1, :_a@1}, {:var, 1, :_b@1}}
  172. iex> ExToErl.elixir_source_to_erlang_abstract_code("import Bitwise; a &&& b")
  173. {:op, 1, :band, {:var, 1, :_a@1}, {:var, 1, :_b@1}}
  174. Some expressions may raise warnings, although they should be the same wanings
  175. as if the Elixir expression were to be compiled inside a normal Elixir module:
  176. iex> ExToErl.elixir_source_to_erlang_abstract_code("a = b")
  177. warning: variable "a" is unused
  178. warning: variable "a" is unused
  179. {:match, 1, {:var, 1, :_a@2}, {:var, 1, :_b@1}}
  180. Some Elixir operators are actually macros or special forms which can be expanded
  181. into quite complex Erlang code:
  182. iex> ExToErl.elixir_source_to_erlang_abstract_code("a or b")
  183. {:case, 1, {:var, 1, :_a@1},
  184. [
  185. {:clause, [generated: true, location: 1], [{:atom, 0, false}], [],
  186. [{:var, 1, :_b@1}]},
  187. {:clause, [generated: true, location: 1], [{:atom, 0, true}], [],
  188. [{:atom, 0, true}]},
  189. {:clause, [generated: true, location: 1], [{:var, 1, :__@1}], [],
  190. [
  191. {:call, 1, {:remote, 1, {:atom, 0, :erlang}, {:atom, 1, :error}},
  192. [{:tuple, 1, [{:atom, 0, :badbool}, {:atom, 0, :or}, {:var, 1, :__@1}]}]}
  193. ]}
  194. ]}
  195. """
  196. def elixir_source_to_erlang_abstract_code(elixir) do
  197. ast = Code.string_to_quoted!(elixir)
  198. elixir_ast_to_erlang_abstract_code(ast)
  199. end
  200. @doc ~S"""
  201. Converts a string containing Elixir code into Erlang source code.
  202. This function expects an Elixir expression.
  203. If you supply a block (which is a valid Elixir expression), only the last one
  204. will be converted into an Erlang expression.
  205. This limitation is a result of the fact that in Erlang a sequence of instructions
  206. if not a an Erlang expression (on the other hand, a sequence of Elixir expressions
  207. is an Elixir expression).
  208. Don't use this function to convert entire Elixir modules to Erlang source code.
  209. Use `ExToErl.beam_to_erlang_source/1` instead.
  210. The function raises if the string is not valid Elixir.
  211. As with most functions in this module, this function *creates atoms at runtime*
  212. because valid Erlang AST contains atoms.
  213. ## Examples
  214. iex> ExToErl.elixir_source_to_erlang_source("a")
  215. "_a@1\n"
  216. iex> ExToErl.elixir_source_to_erlang_source("a + b")
  217. "_a@1 + _b@1\n"
  218. iex> ExToErl.elixir_source_to_erlang_source("a + b < f.(x)")
  219. "_a@1 + _b@1 < _f@1(_x@1)\n"
  220. iex> ExToErl.elixir_source_to_erlang_source("a or b") |> IO.puts()
  221. case _a@1 of
  222. false -> _b@1;
  223. true -> true;
  224. __@1 -> erlang:error({badbool, 'or', __@1})
  225. end
  226. :ok
  227. iex(3)> ExToErl.elixir_source_to_erlang_source("a.b") |> IO.puts()
  228. case _a@1 of
  229. #{b := __@1} -> __@1;
  230. __@1 when erlang:is_map(__@1) ->
  231. erlang:error({badkey, b, __@1});
  232. __@1 -> __@1:b()
  233. end
  234. :ok
  235. """
  236. def elixir_source_to_erlang_source(elixir) do
  237. abstract_code = elixir_source_to_erlang_abstract_code(elixir)
  238. erlang_abstract_code_to_string(abstract_code)
  239. end
  240. @doc """
  241. Converts Elixir AST into Erlang abstract code.
  242. This function expects an Elixir expression.
  243. If you supply a block (which is a valid Elixir expression), only the last one
  244. will be converted into an Erlang expression.
  245. This limitation is a result of the fact that in Erlang a sequence of instructions
  246. if not a an Erlang expression (on the other hand, a sequence of Elixir expressions
  247. is an Elixir expression).
  248. As with most functions in this module, this function *creates atoms at runtime*
  249. because valid Erlang AST contains atoms.
  250. ## Examples
  251. iex> ExToErl.elixir_ast_to_erlang_abstract_code({:+, [line: 1], [{:a, [line: 1], nil}, {:b, [line: 1], nil}]})
  252. {:op, 1, :+, {:var, 1, :_a@1}, {:var, 1, :_b@1}}
  253. iex> Code.string_to_quoted!("a - b") |> ExToErl.elixir_ast_to_erlang_abstract_code()
  254. {:op, 1, :-, {:var, 1, :_a@1}, {:var, 1, :_b@1}}
  255. """
  256. def elixir_ast_to_erlang_abstract_code(ast) do
  257. variables = extract_variables_from_elixir_ast(ast)
  258. module_body =
  259. quote do
  260. @moduledoc "Just a temporary place to store some Erlang abstract code"
  261. def main(unquote_splicing(variables)) do
  262. unquote(ast)
  263. end
  264. end
  265. {:module, module_name, _, _} = module = Module.create(@sandbox_module, module_body, __ENV__)
  266. full_module_abstract_code = beam_to_erlang_abstract_code(module)
  267. function = find_function_by_name(full_module_abstract_code, :main)
  268. body = extract_body_from_function_clause(function)
  269. # Delete the module to avoid the annoying warning about redefining modules.
  270. # Because functions in this module will never be called, there's no need to purge the module.
  271. :code.purge(module_name)
  272. true = :code.delete(module_name)
  273. body
  274. end
  275. @doc """
  276. Parses an Erlang expression into erlang abstract code.
  277. ## Examples
  278. iex> ExToErl.erlang_source_to_abstract_code("A + B.")
  279. {:op, 1, :+, {:var, 1, :A}, {:var, 1, :B}}
  280. iex> ExToErl.erlang_source_to_abstract_code("A < B.")
  281. {:op, 1, :<, {:var, 1, :A}, {:var, 1, :B}}
  282. iex> ExToErl.erlang_source_to_abstract_code("A + B * C < F + G.")
  283. {:op, 1, :<,
  284. {:op, 1, :+, {:var, 1, :A}, {:op, 1, :*, {:var, 1, :B}, {:var, 1, :C}}},
  285. {:op, 1, :+, {:var, 1, :F}, {:var, 1, :G}}}
  286. iex> ExToErl.erlang_source_to_abstract_code("A + B * C < f(x) + g(y).")
  287. {:op, 1, :<,
  288. {:op, 1, :+, {:var, 1, :A}, {:op, 1, :*, {:var, 1, :B}, {:var, 1, :C}}},
  289. {:op, 1, :+, {:call, 1, {:atom, 1, :f}, [{:atom, 1, :x}]},
  290. {:call, 1, {:atom, 1, :g}, [{:atom, 1, :y}]}}}
  291. iex(9)> ExToErl.erlang_source_to_abstract_code("A + B * C < f(X) + g(Y).")
  292. {:op, 1, :<,
  293. {:op, 1, :+, {:var, 1, :A}, {:op, 1, :*, {:var, 1, :B}, {:var, 1, :C}}},
  294. {:op, 1, :+, {:call, 1, {:atom, 1, :f}, [{:var, 1, :X}]},
  295. {:call, 1, {:atom, 1, :g}, [{:var, 1, :Y}]}}}
  296. """
  297. def erlang_source_to_abstract_code(bin) do
  298. charlist = String.to_charlist(bin)
  299. {:ok, tokens, _} = :erl_scan.string(charlist)
  300. {:ok, [expression]} = :erl_parse.parse_exprs(tokens)
  301. expression
  302. end
  303. @doc """
  304. Pretty prints Erlang abstract code as Erlang source code.
  305. ## Examples
  306. TODO
  307. """
  308. def erlang_abstract_code_to_string(abstract_code, opts \\ []) do
  309. indent = Keyword.get(opts, :indent, 8)
  310. [:erl_prettypr.format(abstract_code), "\n"]
  311. |> to_string()
  312. |> String.replace("\t", String.duplicate(" ", indent))
  313. end
  314. # ----------------------------------
  315. # Private functions
  316. # ----------------------------------
  317. defp find_function_by_name(forms, name) do
  318. Enum.find(forms, fn form ->
  319. case form do
  320. {:function, _line, ^name, _arity, clauses} when is_list(clauses) ->
  321. true
  322. _ ->
  323. false
  324. end
  325. end)
  326. end
  327. # Extracts the list of variables form an Elixir AST fragment
  328. defp extract_variables_from_elixir_ast(ast) do
  329. {_ast, variables} =
  330. Macro.postwalk(ast, [], fn ast_node, variables ->
  331. case ast_node do
  332. {name, _meta, module} = variable when is_atom(name) and is_atom(module) ->
  333. {ast_node, [variable | variables]}
  334. _other ->
  335. {ast_node, variables}
  336. end
  337. end)
  338. variables
  339. end
  340. defp extract_body_from_function_clause({:function, _line, _name, _arity, [clause]}) do
  341. {:clause, _line, _args, _guards, body} = clause
  342. List.last(body)
  343. end
  344. end