How to view module source after code generation?

Hello!
I am looking for a way to inspect generated code.

Given this module source:

defmodule Xyz do
  @names [:a, :b, :c]

  for name <- @names do
    def hello(unquote(name)), do: "hello #{unquote(name)}"
  end
end

How can I use the Code / Macro / other modules to produce the resulting module source code, like this?

defmodule Xyz do
  @names [:a, :b, :c]

  def hello(:a), do: "hello a"
  def hello(:b), do: "hello b"
  def hello(:c), do: "hello c"
end

Ideally the solution would also handle injection of code in modules at runtime as well.

1 Like

On my phone so canā€™t try it, but what do you get reading in the source piped to Code.string_to_quoted piped to Macro.expand_once?

src
|> Code.string_to_quoted()
|> then(fn {:ok, ast} -> ast end)
|> Macro.expand_once(__ENV__)

Just gives me an AST with the for loop. Not the produced functions.

Using Macro.prewalk(&Macro.expand(&1, __ENV__)) does not seem to help either.

When you compile this code into beam file calling elixirc xyz.exs then you can convert it into Erlang code using this sample:

defmodule Example do
  def sample(path) when is_binary(path), do: path |> String.to_charlist() |> sample()

  def sample(path) do
    {:ok, {_, [{:abstract_code, {_, abstract_code}}]}} = :beam_lib.chunks(path, [:abstract_code])
    erlang_code = abstract_code |> :erl_syntax.form_list() |> :erl_prettypr.format()
    :io.fwrite(~c"~s~n", [erlang_code])
  end
end

Example.disassemble("Elixir.Xyz.beam")

The result looks like:

-file("xyz.exs", 1).

-module('Elixir.Xyz').

-compile([no_auto_import]).

-export(['__info__'/1, hello/1]).

-spec '__info__'(attributes |
                 compile |
                 functions |
                 macros |
                 md5 |
                 exports_md5 |
                 module |
                 deprecated |
                 struct) -> any().

'__info__'(module) -> 'Elixir.Xyz';
'__info__'(functions) -> [{hello, 1}];
'__info__'(macros) -> [];
'__info__'(struct) -> nil;
'__info__'(exports_md5) ->
    <<"Ā³g\003\227Ā¬]Ć”\210\003nƐƖĆŖ\2044Ć³">>;
'__info__'(Key = attributes) ->
    erlang:get_module_info('Elixir.Xyz', Key);
'__info__'(Key = compile) ->
    erlang:get_module_info('Elixir.Xyz', Key);
'__info__'(Key = md5) ->
    erlang:get_module_info('Elixir.Xyz', Key);
'__info__'(deprecated) -> [].

hello(a) -> <<"hello #{unquote(name)}">>;
hello(b) -> <<"hello #{unquote(name)}">>;
hello(c) -> <<"hello #{unquote(name)}">>.

Iā€™m not in topic, so I have no idea how we can turn original code or Erlang code into generated Elixir code.

2 Likes

Right ā€” my suggestion was doomed to fail. The for and the defs are happening in the ā€œsame stepā€, so to speak, so thereā€™s no discrete step to macroexpand.

If you can change the way the source is generated by abstracting the for loop into an external macro that returns quoted functions, youā€™d be able to expand that and see the result.

def_all(@names)

ā€¦

defmacro def_all(names) do
  for name <- names do
    quote(do: def hello(unquote(name)), do: ā€¦)
  end
end

Just spitballing, still not in a position to test.

1 Like

It might be possible to use decompile to go to erlang/beam code level and back to elixir.

2 Likes

Thanks, that works. :heart: Long way to go to Elixir code but quite interesting nonetheless.

While I donā€™t mind doing that if it was an isolated need and I can see how it could work, sadly I need a solution I can apply to any module e.g. Phoenix Router, Absinthe resolvers, homegrown metaprogramming modules like the one I cited in OP etc.

Is there actually a way to transpile Erlang to idiomatic Elixir? :107: If so, that would be great!

I have tried this project. Actually as long as you have beam file (no idea about Erlang code) you can call it this way:

elixirc xyz.exs
mix decompile Xyz --to expanded

Here is an example result (Elixir.Xyz.ex file):

defmodule Xyz do
  def hello(:a) do
    <<"hello ", String.Chars.to_string(:a)::binary>>
  end

  def hello(:b) do
    <<"hello ", String.Chars.to_string(:b)::binary>>
  end

  def hello(:c) do
    <<"hello ", String.Chars.to_string(:c)::binary>>
  end
end
1 Like

Thatā€™s pretty good, thanks!

For posterity, this involves installing @michalmuskalaā€™s GitHub - michalmuskala/decompile first as @LostKobrakai alluded to.

I got the output that you posted and the only thing thatā€™s slightly puzzling is ā€“ why arenā€™t the module attributes visible in the generated source?

Still, marking yours as the solution since it seems so far thereā€™s no better way.

2 Likes

Hi @dimitarvp

why arenā€™t the module attributes visible in the generated source?

The modulus attribute is only available at compile time and is replaced by the assigned value. Here is an example with an additional function and decompile by beam_file:

iex(1)> {:module, _name, binary, _bindings} =
...(1)>   defmodule Xyz do
...(1)>     @names [:a, :b, :c]
...(1)>
...(1)>     def names, do: @names
...(1)>
...(1)>     for name <- @names do
...(1)>       def hello(unquote(name)), do: "hello #{unquote(name)}"
...(1)>     end
...(1)>   end
{:module, Xyz,
 <<70, 79, 82, 49, 0, 0, 6, 252, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 216,
   0, 0, 0, 24, 10, 69, 108, 105, 120, 105, 114, 46, 88, 121, 122, 8, 95, 95,
   105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>,
 [hello: 1, hello: 1, hello: 1]}
iex(2)> binary |> BeamFile.elixir_code!() |> IO.puts()
defmodule Elixir.Xyz do
  def names do
    [:a, :b, :c]
  end

  def hello(:a) do
    <<"hello ", String.Chars.to_string(:a)::binary()>>
  end

  def hello(:b) do
    <<"hello ", String.Chars.to_string(:b)::binary()>>
  end

  def hello(:c) do
    <<"hello ", String.Chars.to_string(:c)::binary()>>
  end
end
:ok
2 Likes

Ah, nice. I have forgotten about that. :person_facepalming: Yep, module attributes are replaced with their value when used. Thanks for the reminder.