†ここではスクリプト解析で構築した構文木をYARVコードにコンパイルする処理を読解したいと思います。†スクリプトの解析が終わると解析したNODEツリーを引数にruby_run_node関数が呼ばれ、その中でruby_exec_node関数が呼ばれます。ruby_exec_node関数ではNODEツリーをYARVコードに変換(rb_iseq_new関数)し実行(rb_iseq_eval関数)しています。†rb_iseq_new関数は最終的にrb_iseq_new_with_bopt_and_opt関数に行き着きます。どんな引数が渡されるのかを見るために呼び出しフローを見てみましょう。†コンパイル処理のエントリーポイントとなる関数はiseq_compile関数です。ローカル変数テーブルと引数を処理した上でCOMPILEマクロ経由で子ノードがiseq_compile_each関数に渡されます。†コンパイルのメイン処理です。NODEのタイプに応じてYARVコードを生成しています。†NODE_CLASSやNODE_DEFNに行き当たるとマクロ経由でnew_child_iseq関数が呼ばれます。new_child_iseq関数はrb_iseq_new_with_opt関数を呼ぶことで再帰的にNODEをYARVコードに変換しています。†compile.cを読んでいるとNODEを変換した結果のYARVコードやラベルがLINK_ELEMENTという構造体に追加されていることがわかります。LINK_ELEMENT構造体はコンパイル時の作業領域で最終的なYARVコードを生成するために必要な情報を格納するものなようです。†YARVコードを追加するのにはADD_INSNマクロファミリーが使われています。ローカル変数を設定するコードは以下のようになります。iseqは関数の引数で渡されたrb_iseq_t*new_insn_body関数はargc引数で指定された数の追加引数を受け取り設定します。compile_data_alloc関数を呼ぶことで必要に応じて領域を拡大します。new_insn_core関数はcompile_data_alloc_insn関数(compile_data_alloc関数のラッパーです)を呼んでINSN構造体の領域を確保しデータを設定します。その後、ADD_ELEM関数が呼び出されリストにYARVコード情報がつながれます。結果、ADD_INSN1の呼び出しで以下のデータが設定されることになります。†メソッド呼び出しにはADD_SEND_Rマクロが使われています。(VALUE)flag))new_insn_send関数は引数を指定してnew_insn_core関数を呼びsend命令を追加しています。引数は以下の5つです。†iseq_compile関数の最後でiseq_setup関数が呼ばれています。この時点ではまだYARVコードは作業領域に置かれています。iseq_setup関数を呼び出すことで最適化が行われた上でrb_iseq_t構造体に情報が詰め込まれます。†この関数はコンパイルオプションに応じてYARVコードの最適化を行っています。デフォルトではpeephole_optimizationとspecialize_instructionが有効です。†この関数ではjump命令のすぐ次にあるラベルに飛ぶ意味のないjump命令の除去などの最適化を行っています。†この関数では演算子メソッドなどのよく出現するsend命令を専用の命令に置き換える最適化を行っています。†この関数は・・・何もやってない?おそらくvm_opts.hのコンパイルオプションを変更すればわかると思いますが今回は追求を止めておきます。実際にはこの関数はiseq_optimize関数から呼び出されていませんが最適化の一部なのでここに書いておきます。unified_insns_dataが空なので動作がトレースできませんが名前の通り複数の命令を1つにしているようです。†この関数も同様にiseq_optimize関数からではなくiseq_setup関数から呼び出されます。コンパイルオプションの都合で有益な情報がないので深追いは止めておきます。†この関数で作業領域に置かれていたYARVコードがrb_iseq_t構造体に設定されます。LINK_ELEMENTをスキャンして各命令の命令長と命令の数を数え、必要なメモリを確保しています。ラベルの場合は今までの合計命令長を位置として設定します。次にもう一度LINK_ELEMENTをスキャンし確保したメモリに命令コードを埋め込んでいます。その際、calc_sp_depth関数を呼び出して各命令を実行した後のスタック長および最大スタック長を計算しています。insn_op_types関数を呼び出すことで各命令の各引数が何を表しているかが取得し、情報の埋め込みを行っています。jump命令でどれだけ飛べばよいかといった情報が埋め込まれます。†この関数ではコンパイル時にADD_CATCH_ENTRYマクロで追加された、ブロック内でbreakされた場合にどのラベルに移動すればよいかといった情報をrb_iseq_t構造体に設定しています。†この関数ではデフォルト値付き引数の設定情報をrb_iseq_t構造体に設定しています。この関数は設定するだけですがデフォルト値の設定はなかなか興味深いですね。コメントをそのまま貼り付けるのが一番理解しやすいので転載します。expr2つまり、b引数が設定されていなかったらbラベルに飛んでexpr1を実行、結果がスタックに積まれることでb引数となるというからくりなようです。確かにRubyって正確にはデフォルト式でデフォルト値を決定するためにメソッド呼び出しとかできますからね*1。†この関数では設定した命令コードをより実行に適した形に変換しています。デフォルトだとOPT_DIRECT_THREADED_CODEが有効です。vm_eval関数が書かれているvm_evalbody.cを見るとなかなか難解なコードが書かれていますが簡単に言うと各命令処理ルーチンの開始アドレスが格納されます。ところでvm_eval関数は2引数なのですがcompile.cからは1引数で呼ばれてます。手動コンパイルした結果が合っているか調べるには実際にコンパイルされるコードと比較してみることです。VM::InstructionSequence.compile_file(file).disasmとするとfileで指定したスクリプトがコンパイルされ、YARVコードが表示されます。falseとすると最適化が行われません。inline_const_cacheは変換時にコードが埋め込まれるのでinline_const_cacheもオフにしてしまうと手動コンパイル結果と食い違ってしまいます。true}とするとinline_const_cacheを有効にできます。実際にNODEをコンパイルしてみると動きがよくわかるでしょう。スクリプト解析を読むで作成したNODEツリーを変換してみましょう。†NODEツリーの頂点はNODE_SCOPEです。いうわけでiseq_compile関数ではiseq_set_local_table関数が呼ばれてlocal_tableが設定されます。1次にiseq_set_arguments関数が呼ばれますが引数はないのでelseの方が実行されます。1次のswitch文ではISEQ_TYPE_TOPなので単純にCOMPILEマクロが呼ばれます。debug_compileは無視するとしてiseq_compile_each関数が呼ばれます。どのような引数が渡されるのか見てみましょう。poped)一連のコンパイルが終わると最後にleaveが追加されます。†NODE_BLOCKのそれぞれの要素に対してCOMPILE_マクロ(先ほどとは違い、_が付いています)が呼び出されています。popedはリストの最後だけ0でそれ以外は1なようです*2。†ブロックの一番初めはNODE_CLASSなのでNODE_CLASSの処理に移りましょう。NEW_CHILD_ISEQVALマクロが実行されます。引数は以下の通りです。ISEQ_TYPE_CLASS次にcompile_cpath関数が呼ばれています。cpathはNODE_COLON2でcpath->nd_headは0なのでputnilが追加されます。次にnode->nd_superが指定してCOMPILEマクロが呼ばれますがnd_superは0なので同様にputnilが追加されます。iseqvalはNEW_CHILD_ISEQVALマクロで作られたクラス定義です。最後にpopedが1なのでpopが追加されます。iseq_compile_eachと再帰呼び出しされてまたiseq_compile_each関数にやってきます。NODE型はNODE_DEFNなのでNEW_ISEQVALマクロが実行されます。ISEQ_TYPE_METHOD次にputnilが追加されます。最後にpopedが0なのでputnilが追加されます。†というわけでまた再帰呼び出しされてiseq_compile関数に来ます。今回は引数があるのでiseq_set_arguments関数を眺めることにしましょう。NODEに従って読み進めていくと以下のようになりました。†それではNODE_DEFN以下のコンパイルに進みましょう。get_local_var_idx関数を実行することで変数のIDがlocal_tableの何番目かが返ってきます。countは1番目(0始まり)です。次にrhsを指定してCOMPILEマクロが実行されます。NODE_LITなのでputobject(0)が追加されます。†NODE_BLOCKの次の要素はNODE_ITERです。ブロックの先頭に戻るためのラベルが追加されます。次にブロックのNODEツリーを引数にNEW_CHILD_ISEQVALマクロが呼ばれて結果がcompile_data->current_blockに設定されています。ISEQ_TYPE_BLOCK次にメソッド呼び出しの部分((1..n).each)がCOMPILEマクロにかけられます。NODE_CALLがsendに変換され、ブロックとして上で変換したInstructionSequenceオブジェクトが指定されます。次にブロックを抜けるためのラベルが追加されます。次にpopedが1なのでpopが追加されます。最後にADD_CATCH_ENTRYマクロを使用してcompile_data->catch_table_aryに情報を追加しています。catch_table_aryはbreakなどの場合にどこに移動するかを識別する情報のようです。NODE_LASGNに似てますが少し違うようです。rhsを指定してCOMPILEマクロが実行されています。変数のIDからインデックスを取得するのにget_dyna_var_idx関数が呼ばれています。get_local_var_idxとの違いは現在の変数テーブルに変数がなかったら親を探してさかのぼったレベルを返すところです。†rhsはNODE_VCALLです。SUPPORT_JOKEは飛ばして:-)、まずレシーバが設定されています。ADD_CALL_RECEIVERマクロを展開するとputnilが追加されるようです。次に引数の設定ですがNODE_VCALLなので何も追加されません。その後、flagにVM_CALL_VCALL_BITとVM_CALL_FCALL_BIT(コメントにも書いてありますがVCALLは同時にFCALLなのでFCALL_BITも設定されます。case文はbreakで抜けていません)が設定され、ADD_SEND_Rマクロが呼び出されてsendが追加されます。†まずnode->nd_condがコンパイルされます。NODE_CALLなのでそのままCOMPILEマクロに通されます。次にbranchunless(elseラベル)とjump(thenラベル)が追加されます。次にthenラベルが追加され、thenの部分をコンパイル、jump(endラベル)が追加されます。次にelseラベルが追加され、elseの部分がコンパイルされます。最後にendラベルが追加されます。NODE_VCALLに似ていますが今回はレシーバも引数もあります。レシーバがCOMPILEマクロにかけられます。x)までNODE_CALLなのですが先に進みます。次にsetup_args関数が呼ばれて引数が追加されます。引数はNODE_ARRAYなのでcompile_array関数(compile_array_関数のラッパーでpopedに0が渡されています)にかけられます。要素はNODE_LITが1つだけなのでputobject(1)が追加されます(opt_pがQfalseでpopedが0なのでnewarray(1)が追加されますが、setup_args関数に戻ってくると取り除かれます)。その後、ADD_SEND_Rマクロが呼ばれてsendが追加されます。次にレシーバ部を何回か再帰してNODE_DVAR(x)に行き着きます。始点(1)と終点(n)がCOMPILEマクロにかけられ、その後にnewrange(0)が追加されます。†メソッド定義の最後のNODE_BLOCKはNODE_CALLです。popedが0なのでpopが追加されません。ここまで見てきて気づいた人は気づいたと思うのですがどうやらYARVはスタックマシンのようです。した結果、スタックの一番上にある値がメソッドの戻り値になるというわけですね。†しばらくは既存の知識でどうにかなるとして、NODE_CONST(MonteCarlo)もスルーしようかなと思ったらおもしろいことをしてるので書いておきます。compile->option->inline_const_cacheが有効(デフォルトだと有効)の場合、以下のような命令コードが追加されます。終了ラベル)getconstant(:MonteCarlo)setinlinecache(開始ラベル):終了ラベル見てわかるようにキャッシュされてたらgetconstantがバイパスされています。Rubyではクラスは定数でクラスを参照することは多いからという理由でこの最適化がされてると思われます。compile_dstr関数が呼ばれています。その後、NODE_ARRAYの各要素がCOMPILEマクロにかけられます。1)とtostringが追加されます。最後にconcatstrings(2)が追加されます。†というわけでNODEからYARVコードへのコンパイルが完了しました。コンパイル結果と、peepholeおよびspecialized_instruction最適化を施した結果を示します。見方は以下のようになります。今回はNODEツリーからYARVコードへのコンパイルを見てきました。コンパイル処理では各NODEを素直に変換した後、最適化を行い、最終的なバイトコード*3に変換を行っています。それではコードの実行へ進みましょう。