前回の記事では、字句解析、そして構文解析までをご紹介しました。
今回の記事は前回の内容がモリモリに必要になるので、読んでいない方がいらっしゃいましたら以下の記事をご覧ください。
前回の記事で以下のような画像を提示しました。
この続きが以下の画像になります
今回は、構文解析の次である「コンパイル」~「YARV」までを見ていこうとおもいます。
こちらも中々の重厚感があるのでめげずに頑張っていきましょう!
YARV
まず、新たに出てきたYARVという用語について紹介しましょう。
RubyがRubyコードを実行する仮想マシンであるYARV(Yet Another Ruby Virtual Machine)が、Rubyには導入されています。つまり、YARVというのはRubyコードをコンパイルするための仮想マシンのことです。
YARVが使われる際には、コードを仮想マシンが理解できるバイトコードに変換する必要があります。
そしてそれを機械語にするという流れです。図示すると以下のようなことです。
そして重要な点は、YARVはスタックマシンであることです。
スタックについてはこちらが参考になるので読んでみてください。
どうコンパイルされるか?
まず、YARVに向けてコンパイルするには3つのルールがあります。
1 レシーバをプッシュする
2 引数をプッシュする
3 メソッドを呼び出す
では、構文解析されたコードがどのようにコンパイルされて、YARVが期待するコードへと変換されるのかを簡単なコードを例に見ていきましょう
require 'ripper'
require 'pp'
code = <<END
puts 2 + 3
END
puts RubyVM::InstructionSequence.compile(code).disasm
このコードは2 + 3がどんなYARV命令を生成するのかを表示するコードです。結果が以下のようになります。
0000 putself ( 1)[Li]
0001 putobject 2
0003 putobject 3
0005 opt_plus <calldata!mid:+, argc:1, ARGS_SIMPLE>
0007 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
0009 leave
一番上から見ていきましょう
putself
はself(レシーバ)をスタックに置く(put)するという意味です。
先にあげた3つのルールのうちの1つ目である「レシーバをプッシュする」が満たされました
putobject
はスタックにオブジェクトを置く(put)するという意味で、今回でいえば2と3をスタックに置きます。
プッシュするというのが一般的かもしれないです。
これはputの引数です。
opt_puls <calldata!mid:+, argc:1, ARGS_SIMPLE>
はスタックに置いた2と3を呼び出してプラスする。
つまり5という結果が返ってくる。
ブロックはどうか?
先ほどはカンタンな計算を見ていきました。
では、もう少し複雑なブロックはどうYARVに置き換えられるのかを見ていきましょう。
code = <<END
10.times do |n|
puts n
end
END
puts RubyVM::InstructionSequence.compile(code).disasm
以下のような結果が返ってきます。
== disasm: #<ISeq:<compiled>@<compiled>:1 (1,0)-(3,3)> (catch: FALSE)
== catch table
| catch type: break st: 0000 ed: 0005 sp: 0000 cont: 0005
| == disasm: #<ISeq:block in <compiled>@<compiled>:1 (1,9)-(3,3)> (catch: FALSE)
| == catch table
| | catch type: redo st: 0001 ed: 0006 sp: 0000 cont: 0001
| | catch type: next st: 0001 ed: 0006 sp: 0000 cont: 0006
| |------------------------------------------------------------------------
| local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
| [ 1] n@0<Arg>
| 0000 nop ( 1)[Bc]
| 0001 putself ( 2)[Li]
| 0002 getlocal_WC_0 n@0
| 0004 opt_send_without_block <calldata!mid:puts, argc:1, FCALL|ARGS_SIMPLE>
| 0006 nop
| 0007 leave ( 3)[Br]
|------------------------------------------------------------------------
0000 putobject 10 ( 1)[Li]
0002 send <calldata!mid:times, argc:0>, block in <compiled>
0005 nop
0006 leave ( 1)
ブロックがあるときの特徴として、0002 send <calldata!mid:times, argc:0>, block in <compiled>
があります。
これはtimes
メソッドが渡されたときにこのブロックを呼び出すということを表しています。
そしてそのブロックは、ローカルテーブルというもので別のデータ構造として複製されます。
また、ローカルテーブルという名前からも分かるかもしれないが、ローカル変数などの情報もローカルテーブルに保存されます。
まとめ
まとめると、YARVはこれまで見てきたようなデータ構造をしていて、一番大事なポイントは、スタックマシンであることです。
今回は、コンパイルからYARVまでを見てきました。
次回は、それ以降と、ローカルテーブルについて深堀してみたいとおもいます。
最後に
なぜRubyにはバイトコードが存在するのかという仕組みがイマイチ理解できないのでそれを今後調べていこうとおもいます。
分かる方がいらっしゃいましたら、ご教示いただけますと幸いです。
ではでは。