1. 程式人生 > >[Erlang 0113] Elixir 編譯流程梳理

[Erlang 0113] Elixir 編譯流程梳理

      注意:目前Elixir版本還不穩定,程式碼調整較大,本文隨時失效

     之前簡單演示過如何從elixir ex程式碼生成並執行Erlang程式碼,下面仔細梳理一遍elixir檔案的編譯過程,書接上文,從elixir的程式碼切入,這一次我們主要關注編譯流程,一些細節暫時不展開.  
-module(elixir).
......
start_cli() ->
  application:start(?MODULE),
  %% start_cli() --> ["+compile","m.ex"]
  'Elixir.Kernel.CLI':main(init:get_plain_arguments()).
defmodule Kernel.CLI do
......
  if files != [] do
      wrapper fn ->
        Code.compiler_options(config.compiler_options)
        Kernel.ParallelCompiler.files_to_path(files, config.output,
          each_file: fn file -> if config.verbose_compile do IO.puts "Compiled #{file}" end end)
      end
    else
      { :error, "--compile : No files matched patterns #{Enum.join(patterns, ",")}" }
    end
    這裡呼叫了同一目錄下面的Kernel.ParallelCompiler的方法,注意由於Elixir對於程式碼模組名和檔名沒有一致性的要求,找程式碼的時候要注意一點;對應的ex程式碼模組是parallel_compile,程式碼路徑在: https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/kernel/parallel_compiler.ex 從這裡開始,執行邏輯就回到了Erlang程式碼,:elixir_compiler.file(h)等價的Erlang呼叫程式碼是elixir_compile:file(h).
defmodule Kernel.ParallelCompiler do
.....
  try do
        if output do
          :elixir_compiler.file_to_path(h, output)
        else
          :elixir_compiler.file(h)
        end
        parent <- { :compiled, self(), h }
      catch
        kind, reason ->
          parent <- { :failure, self(), kind, reason, System.stacktrace }
      end
 
  elixir_compiler 完成路徑解析之後,最終呼叫了string(Contents, File)方法.我們將string(Contents, File)方法逐步拆解開,elixir_translator:'forms!'(Contents, 1, File, [])首先通過elixir_tokenizer:tokenize 將程式碼文字解析成為Tokens,然後通過 Forms = elixir_parser:parse(Tokens) 解析成為Elixir AST,注意這裡還不是Erlang Abstract Format,這裡的Forms我們輸出一下看看:
 
-module(elixir_compiler).
 

file(Relative) when is_binary(Relative) ->
  File = filename:absname(Relative),
  { ok, Bin } = file:read_file(File),
  string(elixir_utils:characters_to_list(Bin), File).
 
string(Contents, File) when is_list(Contents), is_binary(File) ->
  Forms = elixir_translator:'forms!'(Contents, 1, File, []),
  quoted(Forms, File).
 
[{defmodule,
   [{line,1}],
   [{'__aliases__',[{line,1}],['Math']},
    [{do,
      {def,[{line,2}],[{sum,[{line,2}],[{a,[{line,2}],nil},{b,[{line,2}],nil}]},
        [{do,{'__block__',[],[
         {'=',[{line,3}],[{a,[{line,3}],nil},123]},
         {'=',[{line,4}],[{a,[{line,4}],nil},2365]},
         {'+',[{line,5}],[
          {a,[{line,5}],nil},{b,[{line,5}],nil}]}
          ]}}]]}}]]}]
      Elixir AST在quoted(Forms, File)函式完成到Erlang Abstract Format Forms的轉換,下面我們跟進quoted(Forms,File)方法,在最近的版本中lexical_tracker加入了Scope中;Scope維護了程式碼的上下文資訊,我們暫時有這樣一個印象即可,我們先把流程走完,下面就要深入eval_forms(Forms, Line, Vars, S)方法了. 附Scpoe定義 https://github.com/elixir-lang/elixir/blob/master/lib/elixir/include/elixir.hrl

-record(elixir_scope, {
  context=nil,             %% can be assign, guards or nil
  extra=nil,               %% extra information about the context, like fn_match for fns
  noname=false,            %% when true, don't add new names (used by try)
  super=false,             %% when true, it means super was invoked
  caller=false,            %% when true, it means caller was invoked
  module=nil,              %% the current module
  function=nil,            %% the current function
  vars=[],                 %% a dict of defined variables and their alias
  backup_vars=nil,         %% a copy of vars to be used on ^var
  temp_vars=nil,           %% a set of all variables defined in a particular assign
  clause_vars=nil,         %% a dict of all variables defined in a particular clause
  extra_guards=nil,        %% extra guards from args expansion
  counter=[],              %% a counter for the variables defined
  local=nil,               %% the scope to evaluate local functions against
  context_modules=[],      %% modules defined in the current context
  macro_aliases=[],        %% keep aliases defined inside a macro
  macro_counter=0,         %% macros expansions counter
  lexical_tracker=nil,     %% holds the lexical tracker pid
  aliases,                 %% an orddict with aliases by new -> old names
  file,                    %% the current scope filename
  requires,                %% a set with modules required
  macros,                  %% a list with macros imported from module
  functions                %% a list with functions imported from module
}).

    quoted方法最近的變化是使用elixir_lexical:run包裝了一下,之前的版本簡單直接,可以先看一下:

quoted(Forms, File) when is_binary(File) ->
  Previous = get(elixir_compiled),
% M:elixir_compiler Previous undefined
  try
    put(elixir_compiled, []),
    eval_forms(Forms, 1, [], elixir:scope_for_eval([{file,File}])),
    lists:reverse(get(elixir_compiled))
  after
    put(elixir_compiled, Previous)
  end.

  現在quoted是這樣的:

quoted(Forms, File) when is_binary(File) ->
  Previous = get(elixir_compiled),
  try
    put(elixir_compiled, []),
    elixir_lexical:run(File, fun
      (Pid) ->
        Scope = elixir:scope_for_eval([{file,File}]),
        eval_forms(Forms, 1, [], Scope#elixir_scope{lexical_tracker=Pid})
    end),
    lists:reverse(get(elixir_compiled))
  after
    put(elixir_compiled, Previous)
  end.

   quoted方法裡面我們需要重點關注的是eval_forms方法,在這個方法裡面完成了Elixir AST到Erlang AST轉換,Elixir表示式通過 elixir_translator:translate被翻譯成對應的Erlang Abstract Format.之後eval_mod(Fun, Exprs, Line, File, Module, Vars)完成對錶達式和程式碼其它部分(比如attribute,等等)進行組合.

eval_forms(Forms, Line, Vars, S) ->
  { Module, I } = retrieve_module_name(),
  { Exprs, FS } = elixir_translator:translate(Forms, S),

  Fun  = eval_fun(S#elixir_scope.module),
  Form = eval_mod(Fun, Exprs, Line, S#elixir_scope.file, Module, Vars),
  Args = list_to_tuple([V || { _, V } <- Vars]),

  %% Pass { native, false } to speed up bootstrap
  %% process when native is set to true
  { module(Form, S#elixir_scope.file, [{native,false}], true,
    fun(_, Binary) ->
    Res = Module:Fun(Args),
    code:delete(Module),
    %% If we have labeled locals, anonymous functions
    %% were created and therefore we cannot ditch the
    %% module
    case beam_lib:chunks(Binary, [labeled_locals]) of
      { ok, { _, [{ labeled_locals, []}] } } ->
        code:purge(Module),
        return_module_name(I);
      _ ->
        ok
    end,

    Res
  end), FS }.
 

  最後完成編譯和載入的重頭戲就在module(Forms, File, Opts, Callback)方法了:

%% Compile the module by forms based on the scope information
%% executes the callback in case of success. This automatically
%% handles errors and warnings. Used by this module and elixir_module.
module(Forms, File, Opts, Callback) ->
  DebugInfo = (get_opt(debug_info) == true) orelse lists:member(debug_info, Opts),
  Final =
    if DebugInfo -> [debug_info];
       true -> []
    end,
  module(Forms, File, Final, false, Callback).

module(Forms, File, RawOptions, Bootstrap, Callback) when
    is_binary(File), is_list(Forms), is_list(RawOptions), is_boolean(Bootstrap), is_function(Callback) ->
  { Options, SkipNative } = compile_opts(Forms, RawOptions),
  Listname = elixir_utils:characters_to_list(File),

  case compile:noenv_forms([no_auto_import()|Forms], [return,{source,Listname}|Options]) of
    {ok, ModuleName, Binary, RawWarnings} ->
      Warnings = case SkipNative of
        true  -> [{?MODULE,[{0,?MODULE,{skip_native,ModuleName}}]}|RawWarnings];
        false -> RawWarnings
      end,
      format_warnings(Bootstrap, Warnings),
      %%%% ModuleName :'Elixir.Math' ListName: "/data2/elixir/m.ex"
      code:load_binary(ModuleName, Listname, Binary),
      Callback(ModuleName, Binary);
    {error, Errors, Warnings} ->
      format_warnings(Bootstrap, Warnings),
      format_errors(Errors)
  end.
 

  到這裡編譯的流程已經走完,附上幾個可能會用到的資料,首先是elixir_parser:parse(Tokens)的程式碼,你可能會奇怪這個模組的程式碼在哪裡?這個是通過elixir_parser.yrl編譯自動生成的模組,你可以使用下面的方法拿到它的程式碼:

Eshell V5.10.2 (abort with ^G)
1> {ok,{_,[{abstract_code,{_,AC}}]}} = beam_lib:chunks("elixir_parser",[abstract_code]).
{ok,{elixir_parser,
[{abstract_code,
........
{...}|...]}}]}}
2> Dump= fun(Content)-> file:write_file("/data/dump.data", io_lib:fwrite("~ts.\n", [Content])) end.
#Fun<erl_eval.6.80484245>
3> Dump(erl_prettypr:format(erl_syntax:form_list(AC))).
ok
4>
 下一步,就要深入幾個非常有意思的細節了 %% TODO
  1. elixir_aliases的設計
  2. elixir_scope 的設計
  3. elixir macro 相關的幾個話題:hygiene unquote_splicing
 今天先到這裡. 最後,小圖一張:

馬背上的Godiva夫人


主人公:戈黛娃夫人Lady Godiva,或稱Godgifu,約990年—1067年9月10日
作者:約翰·柯里爾(John Collier)所繪,約1898年

據說大約在1040年,統治考文垂(Coventry)城市的Leofric the Dane伯爵決定向人民徵收重稅,支援軍隊出戰,令人民的生活苦不堪言。伯爵善良美麗的妻子Godiva夫人眼見民生疾苦,決定懇求伯爵減收徵稅,減輕人民的負擔。Leofric伯爵勃然大怒,認為Godiva夫人為了這班愛哭哭啼啼的賤民苦苦衷求,實在丟臉。Godiva夫人卻回答說伯爵定會發現這些人民是多麼可敬。他們決定打賭——Godiva夫人要赤裸身軀騎馬走過城中大街,僅以長髮遮掩身體,假如人民全部留在屋內,不偷望Godiva夫人的話,伯爵便會宣佈減稅。翌日早上,Godiva夫人騎上馬走向城中,Coventry市所有百姓都誠實地躲避在屋內,令大恩人不至蒙羞。事後,Leofric伯爵信守諾言,宣佈全城減稅。這就是著名的Godiva夫人傳說。