ちょっとRubyの中を覗いてみよう

久保田です。

ある程度プログラムを書いていると、ふと、これはなぜ動いているんだろう。。。ただの文字列じゃないか。。。と思うことがあると思います。

というわけで今回はちょっとだけ、Rubyがどのように動いているかを覗き見してみます。
(Rubyがどういった規則で構文解析をしているかなどには触れません。あくまでYARVに渡されるまでの流れを追っていきます。)

例えば以下のようなRubyのプログラムです。

  • foo.rb
result = 1 + 2
puts result

誰でも3が出力されるとわかるようなプログラムですが、$ ruby foo.rb と動かし、3を出力する間に何が起きているでしょう。

実はRubyでは、渡されたプログラムをそのまま実行しているのではなく、 Rubyというプログラムが理解できる形に変換してから実行しています。

そして変換は、字句解析 -> 構文解析 -> コンパイル という3つのステップを経て変換されていきます。

今回はこの3つのステップを少しずつ追っていきたいと思います。

字句解析

まずは字句解析です。
字句解析はただの文字列であるプログラムを トークン という意味のある単語に分ける仕事をします。
foo.rbの一行目でいうと、
resultという変数 / = という演算子 / 1という数字 / +という演算子 / 2という数字 に分けていきます。
(空白や改行などもトークンとして認識しますが、一旦無視します。)

それでは、実際にfoo.rbを字句解析してみましょう。
Rubyには標準ライブラリとしてRipperというものがあり、Ripperを使うと字句解析や構文解析の結果を渡してくれます。

require 'ripper'
require 'pp'

code =<<EOF
result = 1 + 2
puts result
EOF

pp Ripper.lex(code)

結果

[[[1, 0], :on_ident, "result"],
 [[1, 6], :on_sp, " "],
 [[1, 7], :on_op, "="],
 [[1, 8], :on_sp, " "],
 [[1, 9], :on_int, "1"],
 [[1, 10], :on_sp, " "],
 [[1, 11], :on_op, "+"],
 [[1, 12], :on_sp, " "],
 [[1, 13], :on_int, "2"],
 [[1, 14], :on_nl, "\n"],
 [[2, 0], :on_ident, "puts"],
 [[2, 4], :on_sp, " "],
 [[2, 5], :on_ident, "result"],
 [[2, 11], :on_nl, "\n"]]

配列の入れ子でプログラムが トークン に分けられていますね。
一つ目の配列を見て、構成を見てみます。

[[1, 0], :on_ident, "result"]

配列の最小の要素に、プログラム中の[何行目, 何文字目]かが格納されています。
次に、このトークンの種類が入っています。
最後の要素には実際に使われている文字列が入っています。

字句解析はこのように行なわれて、意味のある文字列に分けられているようですね。
ただこの状態では、まだ文としては意味を持っていません。
なので、字句解析の後、Rubyはこの分けられたトークンを元に構文解析を行います。

構文解析

構文解析では、意味のある文字に分けられたRubyプログラムを意味のある文にグループ化し、ASTノードにしていきます。つまり、 パース をします。
パースは、パーサジェネレータを使って行います。Rubyではbisonを使ってパーサジェネレータを作成します。

早速どのような動作をするか確認してみます。

require 'ripper'
require 'pp'

code =<<EOF
result = 1 + 2
puts result
EOF

pp Ripper.sexp(code)

先ほどの字句解析のRipper.lexをRipper.sexpに変更しただけです。

結果

[:program,
 [[:assign,
   [:var_field, [:@ident, "result", [1, 0]]],
   [:binary, [:@int, "1", [1, 9]], :+, [:@int, "2", [1, 13]]]],
  [:command,
   [:@ident, "puts", [2, 0]],
   [:args_add_block, [[:var_ref, [:@ident, "result", [2, 5]]]], false]]]]

先程よりはちょっと複雑な入れ子の配列になっています。
これがASTノードと呼ばれる構造です。
一部を抜き出すと、こういった構造ですね。

f:id:AdwaysEngineerBlog:20170317135428p:plain

少しだけ見ていきます。
まず、配列の最初の要素に:programと書かれています。始まるよー!的な意味ですね。
そして同じ階層のもう一つの要素は配列になっており、その配列の要素は二つありどちらも配列です。
その配列の構造も[シンボル: [配列]]となっているようです。

ここではASTに関しては詳しくは述べませんが、
それぞれの配列の最初のシンボルになっている要素が グループ化された文の種類 を表し、後に続く要素が配列の場合はさらにグループ化されています。 そして続く要素が配列でなくなった時に解析が終了しています。

このように 意味のある文字列(トークン) 意味のある文(ASTノード) にしています。
ここまで来てやっと、Rubyはどのようにただの文字列であるプログラムをどのように扱っていいかがわかるというわけです。

実は、このRuby1.9以前はこの状態まででプログラムの変換は終了だったそうです。
しかしこのままでは実行速度に問題あったらしく、Ruby1.9から笹田耕一さんにより開発されたYARVというRubyの仮想マシンが組みこまれました。

そのYARVがプログラムを実行するため、先ほどの構文木をコンパイルし、YARVが実行できる形式に変換します。

コンパイル

3つのステップの最後、コンパイルです。
早速やってみましょう。

code =<<EOF
result = 1 + 2
puts result
EOF

puts RubyVM::InstructionSequence.compile(code).disasm

結果

== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 0@3] s1)
[ 2] result
0000 trace            1                                               (   1)
0002 putobject_OP_INT2FIX_O_1_C_
0003 putobject        2
0005 opt_plus         <callinfo!mid:+, argc:1, ARGS_SKIP>
0007 setlocal_OP__WC__0 2
0009 trace            1                                               (   2)
0011 putself
0012 getlocal_OP__WC__0 2
0014 opt_send_simple  <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0016 leave

さぁ、ついに意味がわからなくなってきました。笑
正直YARVの処理はまだあまり理解していません。。。
YARVの話だけで一ヶ月ブログ書けるくらいのボリュームだと思っています 。。

かろうじてわかるのは、
0007 setlocal_OP__WC__0 2
で変数をセットして、
0012 getlocal_OP__WC__0 2
で呼び出し、
0014 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
でputsメソッドを呼び出しているのだろう、ということくらいですね。笑

数字の後に続いているのがYARV命令というやつで、YARVが何をするかはここで決めています。

ここら辺の話はまた今度できたらな、と思います。

まとめ

Rubyは、プログラムを渡されると実行までに上記の3ステップの長〜い道のりを行くんですね。
いつも何気なく動かしているプログラムですが、ものすごく高度な技術が折り重なってできているんですね。
皆さんも是非自分のプログラムを覗き見してみてください。

参考文献