ひとり勉強会

ひとり楽しく勉強会

コンパイル処理の流れ

  • iseq = th_compile_from_node(thread, node, file) @ yarvcore.c

解析済みの構文木(node)を受け取って、YARVマシン語列(iseq)に変換する「コンパイル」処理の関数です。
トップレベルのスクリプトコンパイルする場合と、evalで文字列をコンパイルする場合とで少し引数を変えていますが、基本的には、yarv_iseq_new 関数にそのまま処理を丸投げするだけです。実際evalじゃない場合の実行パスだけ見てみると、この通りです。

    VALUE iseq;
    iseq = yarv_iseq_new(node, rb_str_new2("<main>"), file,
                         Qfalse, ISEQ_TYPE_TOP);
    return iseq;
  • yarv_iseq_new(node, name, file, parent, type) @ iseq.c

デフォルトのオプションを指定して、あとはそのままyarv_iseq_new_with_optに丸投げするだけです。オプションでは色々な最適化をON/OFFできます。どんなオプションがあるかについて詳しくは、最適化のコードを読むときに調べます。

    return yarv_iseq_new_with_opt(node, name, file_name,
                                  parent, type, Qfalse,
                                  &COMPILE_OPTION_DEFAULT);
  • yarv_iseq_new_with_opt @ iseq.c

複数の作業を順番にこなしていきます。

    • 命令列を表すRubyオブジェクト(クラスは YARVCore::InstructionSequence) を作成
    • 作ったオブジェクトにファイル名などなど情報を記憶。あと、コンパイル中に必要な作業用メモリの確保など。 (prepare_iseq_build)
    • コンパイル処理のメイン部分 (iseq_compile)
    • 作業用のメモリの解放など (cleanup_iseq_build)
VALUE
yarv_iseq_new_with_opt(NODE*node, VALUE name, VALUE file_name,
		     VALUE parent, VALUE type,
		     VALUE block_opt,
                       const yarv_compile_option_t *option)
{
    yarv_iseq_t *iseq;
    VALUE self = iseq_alloc(cYarvISeq);
    
    GetISeqPtr(self, iseq);
    iseq->self = self;

    prepare_iseq_build(iseq, name, file_name, parent, type,
                       block_opt, option);
    iseq_compile(self, node);
    cleanup_iseq_build(iseq);
    return self;
}

どんどん潜っちゃいましょう。iseq_compileへ。

  • iseq_compile(self, node) @ compile.c

長いのでソースは適当に抜粋です。

VALUE
iseq_compile(VALUE self, NODE *narg)
{
    DECL_ANCHOR(list_anchor);

引数selfは、さっき作ったオブジェクトです。構文木nargをコンパイルして、selfに格納するのがこの関数の仕事です。ここで、selfにいきなりマシン語列を突っ込むことはせずに、一度は命令列をリストで管理するようです。DECL_ANCHORマクロで、リストのデータ構造を宣言しています(後述)。

    debugs("[compile step 1 (traverse each node)]\n");

ここから、ステップごとにデバッグメッセージが埋め込まれててわかりやすいですね!ステップ1は、まず単純に構文木をたどってリストにYARVの命令列をくっつけていくステップです。ノードの種類によって複雑に分岐してますが、最終的に、このステップの処理はCOMPILEというマクロに行きつきます。

	    COMPILE(list_anchor, "top level node", node);

COMPILEマクロの定義は…

#define COMPILE(anchor, desc, node) \
  (debug_compile("== " desc "\n", \
                 iseq_compile_each(iseq, anchor, node, 0)))

デバッグメッセージを記録しつつ、iseq_compile_eachという関数を呼び出しています。iseq_compile_eachは、構文木をたどりながら2000行のswitch文でひたすら命令列を生成しています。具体的な「構文木→命令列」の変換規則についてはまたのちほど。今日はこのあとの流れを先に見てしまいましょう。

    return iseq_setup(iseq, list_anchor);
}

このあとのステップは、iseq_setupという関数に続きます。

  • iseq_setup(iseq, anchor) @

わかりやすいのでデバッグメッセージだけ抜き出してみました。

  //debugs("[compile step 2] (iseq_array_to_linkedlist)\n");
  debugs("[compile step 3.1 (iseq_optimize)]\n");
  debugs("[compile step 3.2 (iseq_insns_unification)]\n");
  debugs("[compile step 3.3 (set_sequence_stackcaching)]\n");
  debugs("[compile step 4.1 (set_sequence)]\n");
  debugs("[compile step 4.2 (set_exception_table)]\n");
  debugs("[compile step 4.3 (set_optargs_table)] \n");
  debugs("[compile step 5 (iseq_translate_direct_threaded_code)] \n");
  debugs("[compile step: finish]\n");

幾つかのステップは、オプションによってif文で実行されたりされなかったりします。ステップ2はコメントアウトされてました。iseq_array_to_linkedlistという名前から察するに、昔はリストではなく配列でステップ1の結果を作っていて、ここではじめてリストに変換していたのでしょうか?