ここではRuby1.9のスクリプト解析を読解したいと思います。
初期化が終わって次はオプションの解析です。Ruby1.8のころと比べるとruby_process_options関数が戻り値を返すようになっています。 また、SAVE_ROOT_JMPBUFマクロでくるまれています。eval_intern.hを見るとfiberに関係しているようですがこれも時が来たら見ることにしましょう。
Ruby1.8ではオプション情報はグローバル変数に格納されていましたがRuby1.9ではcmdline_options構造体に格納されるようになっています。 そのほかの変更点としてオプション解析のメイン処理であるprocess_options関数がrb_vm_call_cfunc関数軽油で呼び出されていますが今のところメリットがわからないので深く気にしないことにします。
RubyGemsを初期化するコードがなかなかおもしろいので書いておきます。短いので全コード掲載。
ruby_init_gems(struct cmdline_options *opt) { VALUE gem; gem = rb_define_module("Gem"); rb_const_set(gem, rb_intern("Enable"), opt->disable_gems ? Qfalse : Qtrue); Init_prelude(); }
というわけでGemモジュールのEnable定数を設定後、Init_prelude関数を呼んでいます。 Init_prelude関数がどこにあるかというとprelude.cに書いてあります。prelude.cを見るとGemの初期化を行うRubyコードが埋め込まれていてrb_iseq_compile, rb_iseq_evalしています。prelude.cはtool/compile_prelude.rbにrbファイルを渡すことで生成されるようです。
スクリプトの解析本体についてはあまり変化はないようですが*1、Ruby1.9での大きな変更点として解析情報がグローバルな変数ではなくローカルな構造体に格納されるようになったみたいです。
NODE* rb_parser_compile_file(volatile VALUE vparser, const char *f, VALUE file, int start) { struct parser_params *parser; volatile VALUE tmp; NODE *node; Data_Get_Struct(vparser, struct parser_params, parser); lex_gets = lex_io_gets; lex_input = file; lex_pbeg = lex_p = lex_pend = 0; node = yycompile(parser, f, start); tmp = vparser; /* prohibit tail call optimization */ return node; }
lex_inputとかは一見グローバルに見えますが実は、
#define lex_input (parser->parser_lex_input)
とdefineされているので引数で渡されたvparserにくるまれたparser_params構造体に情報が格納されています。
次ステップ以降で必要となるので具体的なスクリプトをNODE表現に変換してみましょう。ちなみにRuby1.8とRuby1.9で同じコードを実行してみたところRuby1.9の方が2倍のスピードでした:-)まあC++でもやってみたところC++の方がRuby1.9より4倍速かったですけど
class MonteCarlo def pi(n) count = 0 (1..n).each do x = rand y = rand if x * x + y * y <= 1 count += 1 end end (count.to_f / n) * 4 end end n = 10000 * 10000 pi = MonteCarlo.new.pi(n) puts "pi = #{pi}"
一応、以下の要素があるものにしました。クラスになってる意味がないのですが。
なお、解析の詳細については青木さんのRHGを参照してください。
一番初めの規則はprogramです。初めにlocal_push(はマクロでparser_params構造体が引数に追加されたlocal_push_gen関数)が呼び出されています。compile_for_evalは0です。eval経由で呼び出されると1なのでしょう。local_push_gen関数ではローカル変数のテーブルが初期化されています。
続いて
class MonteCarlo
という部分がprimary規則のうちkeyword_class cpath superclassにマッチします。superclassは指定していないのでsuperclass → termとなり0となります。
cpath規則ではcnameにマッチします。NEW_COLON2マクロによりNODEが一つできあがります。なお、NODE関連の構造体とマクロはinclude/ruby/node.hに定義されています。NODEの値は以下になります。
nd_type = NODE_COLON2 u1.value = 0 u2.value = ID("MonteCarlo") u3.value = 0
次に
def pi(n)
の部分がprimary規則のkeyword_def fnameにマッチします。fnameはtIDENTIFIERになります。local_push_gen関数が呼ばれることで新しいローカル変数スコープが作られています。
primary規則のkeyword_defがマッチするためには次にf_arglist規則がないといけません。でもってf_arglist規則にマッチするためにはf_args規則が必要です。f_args規則の中でマッチするのはf_arg opt_f_block_argです。ブロック引数は受け取っていないのでopt_f_block_argは0です。new_argsマクロが呼ばれてNODEが作られます。の前にf_argを展開することにしましょう。
f_arg規則はf_arg_itemがマッチするので素通りです。 f_arg_item規則ではf_norm_argがマッチします。ローカル変数の引数テーブルの方に変数nが追加された上でNEW_ARGS_AUXマクロによりNODEを作っています。
nd_type = NODE_ARGS_AUX u1.value = ID("n") u2.value = 1 u3.value = 0
さて、f_argが決定したのでnew_args_gen関数が呼ばれてNODEが作られることになります。f_arg opt_f_block_argでの引数は
$$ = new_args($1, 0, 0, 0, $2);
で、new_args_genの宣言は
new_args_gen(struct parser_params *parser, NODE *m, NODE *o, ID r, NODE *p, ID b) {
となっています。opt_f_block_argが0なのでmだけに値が入っていることになります。また、mは先ほど作成したNODE_ARGS_AUXです。
まず、NEW_ARGSマクロによりNODEが作られ、その後に設定が行われています。最終的に以下のようになります。変数nとの関連がなくなってるのが気になりますが先に進みましょう。
nd_type = NODE_ARGS u1.value = 0 u2.value = 1 u3.value = NODE_ARGS_AUX(0, 0)
次は
count = 0
の部分です。bodystmt → compstmt → stmts → stmt → expr → argと進んでいき、arg規則のlhs '=' argがマッチします。
countはlhs規則のvariableにマッチし、variableはtIDENTIFIERにマッチするのでassignable_gen関数が呼ばれてNODEが作られます。ブロックではないローカル変数なのでNODE_LASGNが作られます。
nd_type = NODE_LASGN u1.value = ID("count") u2.value = 0 u3.value = 0
0はarg規則が再帰してprimary → literal → numeric → tINTEGERになるので以下のNODEが作られます。
nd_type = NODE_LIT u1.value = 0 ← リテラル値。値がたまたま0なので0 u2.value = 0 u3.value = 0
rhsのNODEがvalue_expr_gen関数に渡されますがNODE_LITなので何も起こりません。その後、node_assign_gen関数が呼び出されlhsとrhsが関連づけられます。
nd_type = NODE_LASGN u1.value = ID("count") u2.value = NODE_LIT(0) u3.value = 0
次に
(1..n).each do
です。結構めんどくさいです。もっと簡単なのにしとけばよかったかも*2。 一度に説明するのは不可能なので分割して説明します。
まず、(1..n)の()はprimary規則のtLPAREN compstmt ')'にマッチして除去されます。 次に、1..nはarg規則のarg tDOT2 argにマッチします。 1は先ほどと同様にNODE_LITになります。
nの方はarg → primary → var_refとなります。gettable_gen関数が呼ばれてNODEが作られます。
nd_type = NODE_LVAR u1.value = ID("n") u2.value = 0 u3.value = 0
というわけで各要素が決定されたのでNODE構築です。片方がリテラルではないのでNODE_DOT2が作られます。
nd_type = NODE_DOT2 u1.value = NODE_LIT(1) u2.value = NODE_LVAR(ID("n"), 0, 0) u3.value = 0
次にeach doの部分です。primary規則のうちmethod_call brace_blockがマッチします。
eachは引数がないのでmethod_call規則のうちprimary_value '.' operation2 opt_paren_argsがマッチします。opt_paren_argsは0になります。operation2はtIDENTIFIERとなり、primary_valueは先ほど構築したNODE_DOT2になります。以上のことから以下のNODEが作られます。
nd_type = NODE_CALL u1.value = NODE_DOT2 u2.value = ID("each") u3.value = 0
ブロックに入ったのでdyna_push_gen関数が呼ばれて新しくローカル変数のテーブルが追加されています。dyna_push_gen関数では新しいスコープが作られるのではなく既存のテーブルにブロック用のローカル変数テーブルが追加されます。どういうことかというとこういうことです。
lvtbl→ブロック変数テーブル ← dyna_pushで割り当てられる | prev ↓ ローカル変数テーブル−prev→もう一つ外のローカル変数テーブル ↑ local_pushで割り当てられる
次は
x = rand
です。また、arg規則のlhs '=' argがマッチします。
lhs規則は同じくvariableにマッチしますがブロック内変数なので別のNODE型が作られます。
nd_type = NODE_DASGN_CURR u1.value = ID("x") u2.value = 0 u3.value = 0
rhsはvar_ref規則にマッチし、gettable_gen関数が呼び出された結果、引数なしのメソッド呼び出しということがわかるのでNODE_VCALLが作られます*3。
nd_type = NODE_VCALL u1.value = 0 u2.value = ID("rand") u3.value = 0
次の行は同じことなので飛ばしてその次の行、
if x * x + y * y <= 1
これも複雑なので分割して説明します。
x * x + y * y <= 1の部分はarg規則が繰り返し適用されることでボトムアップにNODEが構築されます。x * xだけ説明します。
xはブロック内変数なのでNODE_DVARのNODEが作られます。もう片方のxもNODE_DVARが作られcall_bin_op_gen関数が呼ばれます。結果、NODE_CALLが作られます。
nd_type = NODE_CALL u1.value = NODE_DVAR(ID("x")) u2.value = "*" u3.value = NODE_ARRAY(NODE_DVAR(ID("x")), 1)
最終的にexpr_valueとして以下のようなNODE構造がセットされます。
NODE_CALL NODE_CALL NODE_CALL NODE_DVAR(x) * NODE_ARRAY NODE_DVAR(x) 1 + NODE_ARRAY NODE_CALL NODE_DVAR(y) * NODE_ARRAY NODE_DVAR(y) 1 1 <= NODE_ARRAY NODE_LIT(1) 1
ifを閉じる前に次の行に行きます。
count += 1
自己代入は特殊です。+=というメソッドがあるわけではありません。var_lhsはvariableとなり、ブロック内なのでNODE_DASGNが割り当てられます。また、argはNODE_LITです。tOP_ASGNの部分がどうなるかというと'+'になります。詳しくはparser_yylex関数を参照してください。というわけで以下のようなNODEが構築されます。
NODE_DASGN ID("count") NODE_CALL NODE_DVAR(count) + NODE_ARRAY NODE_LIT(1) 1
arg → expr → stmt → stmtsとなり、上で構築した自己代入NODEを引数にnewline_node関数が呼ばれNODE_NEWLINEフラグが設定されています。次にvoid_stmts_gen関数が呼ばれていますが「使われてないよ〜」というメッセージを表示するものなようです。
endに達したのでNODE_IFが構築できます。if_tailはないので0です。
nd_type = NODE_IF u1.value = NODE_CALL u2.value = NODE_LVAR u3.value = 0
その後、fixpos関数を呼び出すことで行数を調整するようです。
次のendでブロック終了です。opt_block_paramは0で、stmtsは3つあるので少し違います。まず、NODE_DASGN_CURR(y = rand)とNODE_IFがblock_append_gen関数の引数に渡されて以下のNODEが構築されます。
NODE_BLOCK NODE_DASGN_CURR(y = rand) 一番後ろのNODEを参照 NODE_BLOCK NODE_IF 一番後ろのNODE(自分自身)を参照
次にNODE_DASGN_CURR(x = rand)と今作ったNODE_BLOCKがblock_append_gen関数の引数に渡されて以下のNODEが構築されます。
NODE_BLOCK NODE_DASGN_CURR(x = rand) 一番後ろのNODEを参照 NODE_BLOCK NODE_DASGN_CURR(y = rand) 一番後ろのNODEを参照 NODE_BLOCK NODE_IF 一番後ろのNODE(自分自身)を参照
最後にNODE_ITERを作って完成です。
NODE_ITER NODE_SCOPE 変数テーブル NODE_BLOCK 0
method_call、brace_blockが決定したのでprmaryが構築できます。
先ほど作ったNODE_ITERに情報が追加されます。
nd_type = NODE_ITER u1.value = 0 u2.value = NODE_SCOPE u3.value = NODE_CALL((1..n).each)
(count.to_f / n) * 4
の行は今までにわかった知識と努力と根性があればNODE化できるので飛ばします。
endに行き着いたのでメソッド終了です。
bodystmtは以下みたいな感じになります。
NODE_BLOCK NODE_LASGN(count = 0) NODE_BLOCK NODE_ITER NODE_BLOCK NODE_CALL((count.to_f / n) * 4)
remove_begin関数、reduce_node_gen関数では特に変化はないはずです。というわけで以下のNODEが構築されます。
NODE_DEFN ID("pi") NODE_SCOPE 変数テーブル NODE_BLOCK NODE_ARGS
bodystmtはメソッド定義しかないのでそのままNODE_DEFNです。superclassは0なので以下のNODEが作られます。
NODE_CLASS NODE_COLON2(0, MonteCarlo) NODE_SCOPE 変数テーブル NODE_DEFN 0 0
途中は飛ばして次に
puts "pi = #{pi}"
という式展開がどういうNODEに変換されるか見ることにしましょう。まず、strings → string → string1と進んでいくと"に突き当たるのでlex_strtermとして以下のNODEが設定されます。
nd_type = NODE_STRTERM u1.value = str_dquote u2.value = '"' u3.value = 0
次にparser_yylex関数が呼ばれるとparser_parse_string関数が呼ばれることになります。さらにparser_tokadd_string関数が呼ばれ"pi = "と#の前までの文字列が切り出されNODEが作られます。
nd_type = NODE_STR u1.value = "pi = " u2.value = 0 u3.value = 0
次にparser_parse_string関数が呼ばれるとtSTRING_DBEGが返されることになります。{}内が処理されてNODE_LVAR(pi)が作られます。でもってNODE_EVSTRにくるまれます。
nd_type = NODE_EVSTR u1.value = 0 u2.value = NODE_LVAR(pi) u3.value = 0
最後にliteral_concat_gen関数が呼ばれてNODE_STRとNODE_EVSTRが連結されます。NODE_STRなNODEのタイプはNODE_DSTRに変更されます。
NODE_DSTR "pi = " 2 NODE_ARRAY NODE_EVSTR 一番後ろのNODE(自分自身)を参照
最後まで行ったのでprogram規則が終了です。以下のNODEが作られてruby_eval_tree(実際にはparser_paramsのメンバー変数)に設定されます。
nd_type = NODE_SCOPE u1.value = 変数テーブル u2.value = NODE_BLOCK u3.value = 0
以上でNODE表現への変換が終了です。最後に変換されたNODEツリー全てを示します。 montecarlo.node.txt
ここではRuby1.9のスクリプト解析を見てきました。Ruby1.9ではRuby1.8に比べるとオプション情報やスクリプトの解析情報がグローバル変数ではなくローカルな構造体に格納されるようになっています。また、従来のソースの変更を最小限にするための努力も行われています:-)。おそらくYARVというよりRipperによる変更だとは思いますが。
この時点では解析情報はまだ従来の構文木(NODE)表現です。それではYARVコードへのコンパイルに進みましょう。