Ruby
RubyDay 20

Rubyのコードを解析する…?

More than 5 years have passed since last update.

はいこんにちはこんにちは。プログラミング初心者のZonu.EXEです。

今日はRuby Advent Calendar 2012の20日めです…?

ちょっと前まで自宅警備員で、一個月ほど前からアルバイトでるびーおんれーるずを書くお仕事に就いたのですが、Ruby基礎力が低すぎて困ることが多々あります。

そんなわけで、Rubyをきちんと学ぶために基礎の基礎から調べてみましょか、みたいなテンションで書いてました。

ほんとは字句解析だけでがっつりと書きたかったんですけれど、ねたを集める時間がなかったので枝葉末節がひどいです。

タイトルと内容が合ってない気もするけど気にするな! 全然毛色が違ってるねたが混ざってても気にするな! オムニバス!

はじめに

プログラミング言語で書かれたソースコードを読んで何らかの処理を行ふソフトウェアを「処理系」と呼びます。よくわかんないですけど、何らかの処理をしてくれるんですね。処理系はコンパイラだったり、インタプリタだったりします。

Rubyの文脈においては、プログラミング言語の名前はRuby、処理系は本家のRuby、Javaで書かれたJRuby、.NET環境で動くIronRuby、Rubyで書かれたRubiniusがあります。

まあ、ふつうにふつうのRubyで全然問題ありませんね。

本家本元のRubyは言語名と区別するために(コマンド名と同じ)小文字でruby、(Jと対比して)C言語で書かれてるからCRuby、Matzが作った処理系だからMatz Ruby Implementation 、縮めてMRIと呼ばれたりします。
いろんな呼びかたがありますね ヾ(〃><)ノ゙

コード解析とは

別にそんなに難しいことばを持ち出して警戒する必要もないのですが、要するに書いたコードがどんな意味を持つのかを調べるってだけの話です。

コード解析の種類には動的解析静的解析がありますが、別にこれも難しく捉へる必要はないです。「コードだけを見ればわかる」のが静的解析、「実際に動かしてみればわかる」のが動的解析です。

ただし動的解析にはテストフレームワークなどを使ったテストを含みますが、今回はテストについては触れません。RSpecやTest::Unitを使ったテストのことは、いつかもっとえろいひとがしてくれるのでは。

この記事では静的とか動的って話はあまりしない気がします。

環境構築

いまどきMacに標準添付のRuby1.8(賞味期限切れ)はださいので、ちゃんとしたRubyを入れること。

2012年12月20日現在でいちばんナウい安定版はRuby 1.9.3-p327です。2.0 preview2で痛い目に遭っても泣かない子はそっちでもたぶん可。

OS Xでrbenvを使ってruby 1.9.3の環境を作るrbenv & ruby-buildの使い方メモを参考にすると、たいへん良いですね。

餘談ですが、そらはー師匠の記事で触れられてるXcodeの代りにkennethreitz/osx-gcc-installer · GitHubのパッケージを使っても良さげらしいですね。僕自身は未検証なんですけど。

最近ナウい対話環境はPryなので、gem install rdoc pry-docとかやってください。依存でいろいろgemが入ります。

開発環境

Pryが入ったので使ってみませう。コマンドはpryです。rbenvを使ってる人はgem installした後にrbenv rehashを忘れないでね。

Pryは、Rubyに最初からくっついてきてるIRBよりも、さらに強いやつです。色がついたり、キーボードのTABキーでメソッド名補完もできたり、いたれりつくせりですね。

Pryでメソッドの説明を読む

この記事でわざわざPryの話から始めたのは、この説明がしたかったからです。

Pryではshow-docコマンドで、メソッドの説明を読むことができます。

`pry`
pry(main)> show-doc Ripper.lex

From: /Users/megurine/.rbenv/versions/2.0.0-preview1/lib/ruby/2.0.0/ripper/lexer.rb @ line 38:
Number of lines: 18
Owner: #<Class:Ripper>
Visibility: public
Signature: lex(src, filename=?, lineno=?)

Tokenizes the Ruby program and returns an Array of an Array,
which is formatted like [[lineno, column], type, token].

  require 'ripper'
  require 'pp'

  p Ripper.lex("def m(a) nil end")
    #=> [[[1,  0], :on_kw,     "def"],
         [[1,  3], :on_sp,     " "  ],
         [[1,  4], :on_ident,  "m"  ],
         [[1,  5], :on_lparen, "("  ],
         [[1,  6], :on_ident,  "a"  ],
         [[1,  7], :on_rparen, ")"  ],
         [[1,  8], :on_sp,     " "  ],
         [[1,  9], :on_kw,     "nil"],
         [[1, 12], :on_sp,     " "  ],
         [[1, 13], :on_kw,     "end"]]

これ、実際に端末で実行してみてもちゃんと色がつきます。かっこいいですね。

注意なのですが、Rubyレベルではクラスメソッドの呼び出しは、たとへばString::newでもString.newでも良いのですが、(現在のバージョンのpry-docでは)show-docコマンドの引数としてはString.newと書かないとだめです。気をつけてくださいね。

閑話休題。Pryの実装は読んでないのですが、method(:'show-doc')とかやってもMethodオブジェクトを得られないので、Rubyレベルのメソッドではないコマンドみたいですね。

クラスの継承関係を辿る

`pry`
pry(main)> class Foo
pry(main)*   nil
pry(main)* end
=> nil
pry(main)> Foo.ancestors
=> [Foo, Object, PP::ObjectMixin, Kernel, BasicObject]

Class.ancestorsメソッドはクラスの継承関係を配列で並べて返します。Rubyに多重継承はないので、常に直線です。

オブジェクトのメソッド一覧を調べる

`pry`
pry(main)> class Z;end
=> nil
pry(main)> z = Z.new
=> #<Z:0x007f990509cdd8>
pry(main)> z.methods
=> [:pry,
 :__binding__,
 :pretty_print,
 :pretty_print_cycle,
 :pretty_print_instance_variables,
 :pretty_print_inspect,

実際にはもっとたくさんのメソッドが定義されますし、表示することもできます。

じゃあ定義されたメソッドの数を数へみましょっかー、といふときにはどうするのが良いですかね。

`pry`
pry(main)> z.methods.size
=> 63

はい、お手軽にできました。

ここからはちょっとした応用篇。

Fixnumオブジェクトのto_xxxメソッドを列挙したい

Array#grepメソッドと正規表現で、お望みのメソッド名を抽出してみるのが良さげですね。

`pry`
pry(main)> 1.methods.grep(/^to_.*/)
=> [:to_s, :to_f, :to_i, :to_int, :to_r, :to_c, :to_enum]

「シンボルにあって文字列にないメソッド」を列挙したい

配列同士の引き算を利用してらどうですかね。

`pry`
pry(main)> :''.methods - ''.methods
=> [:id2name, :to_proc]
pry(main)> ''.methods - :''.methods
=> "[:+, :*, :%, :[]=, :insert, :bytesize, :succ!, :next!, :upto, :index, :rindex, :replace, :clear, :chr, :getbyte, :setbyte, :byteslice, :to_i, :to_f, :to_str, :dump, :upcase!, :downcase!, :capitalize!, :swapcase!, :hex, :oct, :split, :lines, :bytes, :chars, :codepoints, :reverse, :reverse!, :concat, :<<, :prepend, :crypt, :ord, :include?, :start_with?, :end_with?, :scan, :ljust, :rjust, :center, :sub, :gsub, :chop, :chomp, :strip, :lstrip, :rstrip, :sub!, :gsub!, :chop!, :chomp!, :strip!, :lstrip!, :rstrip!, :tr, :tr_s, :delete, :squeeze, :count, :tr!, :tr_s!, :delete!, :squeeze!, :each_line, :each_byte, :each_char, :each_codepoint, :sum, :slice!, :partition, :rpartition, :force_encoding, :valid_encoding?, :ascii_only?, :unpack, :encode, :encode!, :to_r, :to_c, :shellsplit, :shellescape, :shell_split]

SymbolStringは相互変換可能だけど全然別物なんだぜ? みたいな傍證のひとつでした。

ソースコードを字句解析してみる

字句解析(lexical analysis)とは、文字列をプログラミング言語のソースコードとして認識するためのプロセスのひとつです。

たとへば def f(a,b);nil;end といふ文字の並びを、Rubyはどのように字句解析するのか調べてみませう。

Ruby本体に添付されてゐるRipperといふライブラリを利用することで、ソースコードに対してRubyと同じ字句解析を行うことができます。

Ripperは標準添付のライブラリなので、Rubyをちゃんとインストールされた方ならば、特にインストール作業の必要ありません。

Ripper.tokenizeは、文字列を単純にトークン単位に分解します。 トークンとは、意味をもった文字の塊の単位のことです。

そして、Ripper.lexは、その切り分けられたトークンの位置と、そのトークンの具体的な意味、つまり何者であるかを調べることができます。

`pry`
pry(main)> require 'ripper'
=> true

pry(main)> Ripper.tokenize("def f(a,b);nil;end")
=> ["def", " ", "f", "(", "a", ",", "b", ")", ";", "nil", ";", "end"]

pry(main)> Ripper.lex("def f(a,b);nil;end")
=> [[[1, 0], :on_kw, "def"],
 [[1, 3], :on_sp, " "],
 [[1, 4], :on_ident, "f"],
 [[1, 5], :on_lparen, "("],
 [[1, 6], :on_ident, "a"],
 [[1, 7], :on_comma, ","],
 [[1, 8], :on_ident, "b"],
 [[1, 9], :on_rparen, ")"],
 [[1, 10], :on_semicolon, ";"],
 [[1, 11], :on_kw, "nil"],
 [[1, 14], :on_semicolon, ";"],
 [[1, 15], :on_kw, "end"]]

はい、綺麗に分解してくれましたね。on_kwは、いはゆる予約語です。

on_ident識別子です。この場合でのfはメソッド名、abは仮引数名と素性はあきらかですが、いつでもそうとは限りません。識別子はメソッド呼び出しかもしれませんし、変数名かもしれません。

`pry`
pry(main)> Ripper.lex('a = "foo"')
=> [[[1, 0], :on_ident, "a"],
 [[1, 1], :on_sp, " "],
 [[1, 2], :on_op, "="],
 [[1, 3], :on_sp, " "],
 [[1, 4], :on_tstring_beg, "\""],
 [[1, 5], :on_tstring_content, "foo"],
 [[1, 8], :on_tstring_end, "\""]]

話は逸れますが、(a??a)?:a: :a_?(?:,:'?')は、まったく有効なRubyのコードです。このコードをRipperで字句解析してみたりするとおもしろいですね。
あと、実際にこのコードを動作させるにはどんな準備が必要なのかチャレンジしてみませうヾ(〃><)ノ゙☆

parsetreeで構文木をダンプする

こんな感じのファイルを用意します。

foo1.rb
a = "foo"
puts a

ruby --dump parsetree ./foo1.rb みたいにして実行してみませう。

% vim foo1.rb
% ruby --dump parsetree ./foo1.rb
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 3)
# +- nd_tbl: :a
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (line: 1)
#     +- nd_head:
#     |   @ NODE_DASGN_CURR (line: 1)
#     |   +- nd_vid: :a
#     |   +- nd_value:
#     |       @ NODE_STR (line: 1)
#     |       +- nd_lit: "foo"
#     +- nd_next:
#         @ NODE_BLOCK (line: 2)
#         +- nd_head:
#         |   @ NODE_FCALL (line: 2)
#         |   +- nd_mid: :puts
#         |   +- nd_args:
#         |       @ NODE_ARRAY (line: 2)
#         |       +- nd_alen: 1
#         |       +- nd_head:
#         |       |   @ NODE_DVAR (line: 2)
#         |       |   +- nd_vid: :a
#         |       +- nd_next:
#         |           (null node)
#         +- nd_next:
#             (null node)

構文木とは… とか説明を書いてる時間はないのですが、この構文木はCのデータ構造になってます。Ruby1.8以前はこの構文木を辿って実行されるインタプリタでした。

ruby--dump parsetreeを付けて実行すると、Rubyは実行時の構文木を人間に見やすい形式で出力してくれます。

さて、NODE_DASGN_CURR代入文NODE_DVAR変数の参照、それぞれの下にぶら下がってるnd_vid: :aが変数名、みたいな感じですね。

じゃあ今度はこんなコード。

foo2.rb
def a ()
  return "Foo"
end


puts a

実行してみると…

% ruby --dump parsetree ./foo2.rb
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 7)
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (line: 1)
#     +- nd_head:
#     |   @ NODE_DEFN (line: 1)
#     |   +- nd_mid: :a
#     |   +- nd_defn:
#     |       @ NODE_SCOPE (line: 3)
#     |       +- nd_tbl: (empty)
#     |       +- nd_args:
#     |       |   @ NODE_ARGS (line: 1)
#     |       |   +- nd_frml: 0
#     |       |   +- nd_next:
#     |       |   |   @ NODE_ARGS_AUX (line: 1)
#     |       |   |   +- nd_rest: (null)
#     |       |   |   +- nd_body: (null)
#     |       |   |   +- nd_next:
#     |       |   |       (null node)
#     |       |   +- nd_opt:
#     |       |       (null node)
#     |       +- nd_body:
#     |           @ NODE_STR (line: 2)
#     |           +- nd_lit: "Foo"
#     +- nd_next:
#         @ NODE_BLOCK (line: 6)
#         +- nd_head:
#         |   @ NODE_FCALL (line: 6)
#         |   +- nd_mid: :puts
#         |   +- nd_args:
#         |       @ NODE_ARRAY (line: 6)
#         |       +- nd_alen: 1
#         |       +- nd_head:
#         |       |   @ NODE_VCALL (line: 6)
#         |       |   +- nd_mid: :a
#         |       +- nd_next:
#         |           (null node)
#         +- nd_next:
#             (null node)

よく見てくださいよ奥さん、print aの行はまったく同じはずなのに、出来上がった構文木ではNODE_DVARだった箇所がNODE_VCALLになってますよ… おっかないですね…

じゃあ、このコードのどこかにa = "foo"って足したらどうなるんですかね……?

VMのバイトコードを読む

先程の章で1.8では構文木を辿って実行されるのだ、と書きましたが、1.9ではさらに通称YARVと呼ばれるVMで実行されるバイトコードにコンパイルされて実行されます。

バイトコードとは… って、やっぱりja.Wpでも読んだ方が良いですね。

`pry`
pry(main)> puts RubyVM::InstructionSequence.compile("puts !true").disasm
== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>==========
0000 trace            1                                               (   1)
0002 putself
0003 putobject        true
0005 opt_not          <callinfo!mid:!, argc:0, ARGS_SKIP>
0007 send             <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0009 leave

流れでバイトコードの見方も紹介しましたが、正直僕自身は構文木ほどには心惹かれるものは… けふんけふん。

ちなみにPythonはバイトコード形式にコンパイルしたものが.pyc.pyoの拡張子のファイルとして保存されますが、Rubyにはそれに相当するようなファイル形式は存在せず、毎度毎度バイトコードにコンパイルをしてから実行するようになってます。
Python方式とRuby方式のメリットデメリットを比較してみるとおもしろいかもしれませんねヾ(〃><)ノ゙☆

参考資料

この記事を書く参考にはあまりしてなくて、ふつうにRubyの勉強に役立つんじゃないかなー、って資料ですねヾ(〃><)ノ゙

Rubyソースコード完全解説(RHG)

えっと、バイブルです。通称:RHG

出版された本ですが全文無料で読めます(書籍は入手困難)。ベースのRubyが古いですが、Rubyの基礎(foundation的な意味で)を理解するには、未だにこれ以上ない金字塔。

Rubyist Magazine - YARV Maniacs

若き日のささだこういち先生の書かれた連載記事(の第1回)です。

RHGをつまみ食ひするための手がかりとしても、Ruby1.9と1.8以前の差異のまとめとしても、たいへん勉強になります。