はじめに

ここではRuby1.9のスクリプト解析を読解したいと思います。

ruby_options(eval.c)

初期化が終わって次はオプションの解析です。Ruby1.8のころと比べるとruby_process_options関数が戻り値を返すようになっています。 また、SAVE_ROOT_JMPBUFマクロでくるまれています。eval_intern.hを見るとfiberに関係しているようですがこれも時が来たら見ることにしましょう。

ruby_process_options(ruby.c)

Ruby1.8ではオプション情報はグローバル変数に格納されていましたがRuby1.9ではcmdline_options構造体に格納されるようになっています。 そのほかの変更点としてオプション解析のメイン処理であるprocess_options関数がrb_vm_call_cfunc関数軽油で呼び出されていますが今のところメリットがわからないので深く気にしないことにします。

ruby_init_gems(ruby.c)

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ファイルを渡すことで生成されるようです。

rb_parser_compile_file(parser.y)

スクリプトの解析本体についてはあまり変化はないようですが*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

一番初めの規則はprogramです。初めにlocal_push(はマクロでparser_params構造体が引数に追加されたlocal_oush_gen関数)が呼び出されています。compile_for_evalは0です。eval経由で呼び出されると1なのでしょう。local_push_gen関数ではローカル変数のテーブルが初期化されています。

primary(keyword_class cpath superclass)

続いて

class MonteCarlo

という部分がprimary規則のうちkeyword_class cpath superclassにマッチします。superclassは指定していないのでsuperclass → termとなり0となります。

cpath

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

primary(keyword_def fname)

次に

  def pi(n)

の部分がprimary規則のkeyword_def fnameにマッチします。fnameはtIDENTIFIERになります。local_push_gen関数が呼ばれることで新しいローカル変数スコープが作られています。

f_args(f_arg opt_f_block_arg)

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_item

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

new_args_gen

さて、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)

arg(lhs '=' arg)

次は

    count = 0

の部分です。bodystmt → compstmt → stmts → stmt → expr → argと進んでいき、arg規則のlhs '=' argがマッチします。

lhs(variable)

countはlhs規則のvariableにマッチし、variableはtIDENTIFIERにマッチするのでassignable_gen関数が呼ばれてNODEが作られます。ブロックではないローカル変数なのでNODE_LASGNが作られます。

nd_type = NODE_LASGN
u1.value = ID("count")
u2.value = 0
u3.value = 0

numeric(tINTEGER)

0はarg規則が再帰してprimary → literal → numeric → tINTEGERになるので以下のNODEが作られます。

nd_type = NODE_LIT
u1.value = 0 ← リテラル値。値がたまたま0なので0
u2.value = 0
u3.value = 0

node_assign_gen

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

arg(arg tDOT2 arg)

次に

   (1..n).each do

です。結構めんどくさいです。もっと簡単なのにしとけばよかったかも*2。 一度に説明するのは不可能なので分割して説明します。

まず、(1..n)の()はprimary規則のtLPAREN compstmt ')'にマッチして除去されます。 次に、1..nはarg規則のarg tDOT2 argにマッチします。 1は先ほどと同様にNODE_LITになります。

var_ref

nの方はarg → primary → var_refとなります。gettable_gen関数が呼ばれてNODEが作られます。

nd_type = NODE_LVAR
u1.value = ID("n")
u2.value = 0
u3.value = 0

arg(arg tDOT2 arg)の続き

というわけで各要素が決定されたのでNODE構築です。片方がリテラルではないのでNODE_DOT2が作られます。

nd_type = NODE_DOT2
u1.value = NODE_LIT(1)
u2.value = NODE_LVAR(ID("n"), 0, 0)
u3.value = 0

primary(method_call brace_block)

次にeach doの部分です。primary規則のうちmethod_call brace_blockがマッチします。

method_call(primary_value '.' operation2 opt_paren_args)

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

brace_block(keyword_do)

ブロックに入ったのでdyna_push_gen関数が呼ばれて新しくローカル変数のテーブルが追加されています。dyna_push_gen関数では新しいスコープが作られるのではなく既存のテーブルにブロック用のローカル変数テーブルが追加されます。どういうことかというとこういうことです。

lvtbl→ブロック変数テーブル ← dyna_pushで割り当てられる
           |
          prev
           ↓
       ローカル変数テーブル−prev→もう一つ外のローカル変数テーブル
           ↑
       local_pushで割り当てられる

arg(lhs '=' arg)その2

次は

      x = rand

です。また、arg規則のlhs '=' argがマッチします。

NODE_DASGN_CURR

lhs規則は同じくvariableにマッチしますがブロック内変数なので別のNODE型が作られます。

nd_type = NODE_DASGN_CURR
u1.value = ID("x")
u2.value = 0
u3.value = 0

NODE_VCALL

rhsはvar_ref規則にマッチし、gettable_gen関数が呼び出された結果、引数なしのメソッド呼び出しということがわかるのでNODE_VCALLが作られます*3

nd_type = NODE_VCALL
u1.value = 0
u2.value = ID("rand")
u3.value = 0

primary(keyword_if expr_value then)

次の行は同じことなので飛ばしてその次の行、

      if x * x + y * y <= 1

これも複雑なので分割して説明します。

expr_value

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

arg(var_lhs tOP_ASGN arg)

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

primary(keyword_if expr_value then)の続き

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関数を呼び出すことで行数を調整するようです。

brace_block(keyword_do)の続き

次の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

primary(method_call brace_block)の続き

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)

primary(keyword_def fname)の続き

    (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

primary(keyword_class cpath superclass)の続き

bodystmtはメソッド定義しかないのでそのままNODE_DEFNです。superclassは0なので以下のNODEが作られます。

NODE_CLASS
  NODE_COLON2(0, MonteCarlo)
  NODE_SCOPE
    変数テーブル
    NODE_DEFN
    0
  0

strings

途中は飛ばして次に

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続き

最後まで行ったのでprogram規則が終了です。以下のNODEが作られてruby_eval_tree(実際にはparser_paramsのメンバー変数)に設定されます。

nd_type = NODE_SCOPE
u1.value = 変数テーブル
u2.value = NODE_BLOCK
u3.value = 0

以上でNODE表現への変換が終了です。最後に変換されたNODEツリー全てを示します。 filemontecarlo.node.txt

おわりに

ここではRuby1.9のスクリプト解析を見てきました。Ruby1.9ではRuby1.8に比べるとオプション情報やスクリプトの解析情報がグローバル変数ではなくローカルな構造体に格納されるようになっています。また、従来のソースの変更を最小限にするための努力も行われています:-)。おそらくYARVというよりRipperによる変更だとは思いますが。

この時点では解析情報はまだ従来の構文木(NODE)表現です。それではYARVコードへのコンパイルに進みましょう。


*1 実際にはかなり変わっています。あくまでYARV化されたことに対して、です
*2 読んでたらブロック引数は省略できることがわかったのでなくしたのは内緒:-P
*3 lhs '=' command_callのフローを眺めてたのですがcommand_callは引数が必要なのではずれでした。Rubyの文法ってとても複雑ですね:-)

トップ   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS