It's mentioned in Erlang/Elixir Syntax: A Crash Course that Elixir compiles into BEAM bytecode via Erlang Abstract Format. But actually, there's an extra step between the Erlang Abstract Format and the BEAM bytecode – Core Erlang.
It's a neat little language with a strict syntax that isn't suited for writing by hand but is used as an intermediary format to run optimizations, checks, and simplifications.
Dialyzer also works on the Core Erlang level.
Elixir/Erlang compilation steps
Elixir -> Core Erlang transformation
-
Write an elixir module and save it into
test.ex
:# test.ex defmodule Test do @num 10 def calc(42), do: :ok def calc(list) when is_list(list) do [h | _] = list h + @num + :rand.uniform() end end
-
Compile it to produce
Elixir.Test.beam
file:$> elixirc test.ex
-
Get Erlang Abstract Format from compiled BEAM module:
{:ok, {_, [abstract_code: {_, ac}]}} = Test |> :code.which() |> :beam_lib.chunks([:abstract_code]) ac # erlang abstract format
It'll return the following structure:
[ {:attribute, 1, :file, {'lib/test.ex', 1}}, {:attribute, 1, :module, Test}, {:attribute, 1, :compile, [:no_auto_import]}, {:attribute, 1, :export, [__info__: 1, calc: 1]}, #... __info__ omitted {:function, 6, :calc, 1, [ {:clause, 4, [{:integer, 0, 42}], [], [{:atom, 0, :ok}]}, {:clause, 6, [{:var, 6, :_list@1}], [ [ {:call, 6, {:remote, 6, {:atom, 0, :erlang}, {:atom, 6, :is_list}}, [{:var, 6, :_list@1}]} ] ], [ {:match, 7, {:cons, 0, {:var, 7, :_h@1}, {:var, 7, :_}}, {:var, 7, :_list@1}}, {:op, 8, :+, {:op, 8, :+, {:var, 8, :_h@1}, {:integer, 0, 10}}, {:call, 8, {:remote, 8, {:atom, 0, :rand}, {:atom, 8, :uniform}}, []}} ]} ]} ]}
-
Get Erlang module out of the abstract code and save it into a file:
erl = :erl_prettypr.format(:erl_syntax.form_list(ac)) File.write!("Elixir.Test.erl", erl)
It'll return an ordinary erlang module:
-file("test.ex", 1). -module('Elixir.Test'). -compile([no_auto_import]). -export(['__info__'/1, calc/1]). % ... __info__ omitted ... calc(42) -> ok; calc(_list@1) when erlang:is_list(_list@1) -> [_h@1 | _] = _list@1, _h@1 + 10 + rand:uniform().
Now you can get a clear picture of how Elixir modules map to Erlang code.
But we're not finished just yet.
-
Finally, compile erlang module to get Core Erlang representation:
# Compile erl $> erlc +to_core Elixir.Test.erl
It'll produce
Elixir.Test.core
file:module 'Elixir.Test' ['__info__'/1, 'calc'/1, 'module_info'/0, 'module_info'/1] attributes [%% Line 1 'file' = %% Line 1 [{[69|[108|[105|[120|[105|[114|[46|[84|[101|[115|[116|[46|[101|[114|[108]]]]]]]]]]]]]]],1}], %% Line 1 'file' = %% Line 1 [{[116|[101|[115|[116|[46|[101|[120]]]]]]],1}], %% Line 5 'compile' = %% Line 5 ['no_auto_import'], %% ... '__info__'/1 omitted ... 'calc'/1 = %% Line 23 fun (_0) -> case _0 of <42> when 'true' -> 'ok' %% Line 24 <_X_list@1> when call 'erlang':'is_list' (_0) -> %% Line 25 case _X_list@1 of <[_X_h@1|_5]> when 'true' -> let <_3> = call 'erlang':'+' (_X_h@1, 10) in let <_2> = call 'rand':'uniform' () in call 'erlang':'+' (_3, _2) ( <_1> when 'true' -> primop 'match_fail' ({'badmatch',_1}) -| ['compiler_generated'] ) end ( <_4> when 'true' -> ( primop 'match_fail' ({'function_clause',_4}) -| [{'function_name',{'calc',1}}] ) -| ['compiler_generated'] ) end 'module_info'/0 = fun () -> call 'erlang':'get_module_info' ('Elixir.Test') 'module_info'/1 = fun (_0) -> call 'erlang':'get_module_info' ('Elixir.Test', _0) end
P.S: Further experiments with debug_info.
Prior to OTP 20, BEAM had a special chunk - Abst
- that kept Erlang Abstract Code inside the BEAM files.
OTP 20 introduced new BEAM chunk: Dbgi
that allowed to store Elixir Abstract Code in the BEAM files, instead of Erlang.
It didn't break any of the old code, because :beam_lib.chunks([:abstract_code])
converts Elixir Abstract Code into Erlang on the fly.
We can compare debug_info
metadata for Elixir and Erlang modules to get a better idea of how it works.
Debug_info chunk for Elixir module
Compile Elixir module and get debug_info
chunk:
# compile Elixir module
$> elixirc test.ex
'Elixir.Test.beam' |> :beam_lib.chunks([:debug_info])
You'll get elixir_erl
abstract code in the debug_info:
[
debug_info: {:debug_info_v1, :elixir_erl,
{:elixir_v1,
%{
attributes: [],
compile_opts: [],
definitions: [
{{:calc, 1}, :def, [line: 6],
[
{[line: 4], '*', [], :ok},
{[line: 6], [{:list, [line: 6], nil}],
[
{{:., [], [:erlang, :is_list]}, [line: 6],
[{:list, [line: 6], nil}]}
],
{:__block__, [line: 6],
[
{:=, [line: 7],
[
[
{:|, [line: 7],
[{:h, [line: 7], nil}, {:_, [line: 7], nil}]}
],
{:list, [line: 7], nil}
]},
{{:., [], [:erlang, :+]}, [line: 8],
[
{{:., [], [:erlang, :+]}, [line: 8],
[{:h, [line: 8], nil}, 10]},
{{:., [line: 8], [:rand, :uniform]}, [line: 8], []}
]}
]}}
]}
],
deprecated: [],
file: "**/test.ex",
line: 1,
module: Test,
unreachable: []
}, []}}
]
Debug_info chunk for Erlang module
Compile previous Erlang module and get debug_info
chunk:
# compile Erlang module
$> erlc +debug_info Elixir.Test.erl
'Elixir.Test.beam' |> :beam_lib.chunks([:debug_info])
Now you'll get erlang_abstract_code
directly instead:
[
debug_info: {:debug_info_v1, :erl_abstract_code,
{[
{:attribute, 1, :file, {'Elixir.Test.erl', 1}},
{:attribute, [generated: true, location: 1], :file, {'test.ex', 1}},
{:attribute, 3, :module, Test},
%% ... the same Erlang Abstract Code as above ...
Debug_info chunk for Core Erlang
As final experiment, compile Elixir.Test.core
file:
# Shell
$> erlc +debug_info +to_core Elixir.Test.erl
$> erlc +debug_info Elixir.Test.core
'Elixir.Test.beam' |> :beam_lib.chunks([:debug_info])
There's no abstract_code metadata in debug_info:
[
debug_info: {:debug_info_v1, :erl_abstract_code,
{[], [{:cwd, '/**'}, :debug_info]}}
]