eval_get_cvar_base() と CREF

前回のおさらい。
クラスローカルインスタンス変数を、クラスからしか見えなくするために、クラスとインスタンス変数名の組でしかアクセスできない仕組みが 1.9 で導入された (ivar2)。変数名はそのものずばり "@_a" のような名前だが、クラスは eval_get_cvar_base() という関数で取ってくる。この関数は何をしているのか。エラー処理を除いたら以下のようになる。

eval_get_cvar_base(rb_thread_t *th, rb_iseq_t *iseq)
{
    NODE *cref = get_cref(iseq, th->cfp->lfp);
    VALUE klass = Qnil;

    if (cref) {
	klass = cref->nd_clss;
    ........
    }
    ........
    return klass;
}

(vm.c)

get_cref() で得た cref から klass を取り出して返しているだけだ。cref とはクラスのネスト関係を表わすスタックだ。RHG の14章で説明してある。


その get_cref() は以下のように定義されている。

static NODE *
get_cref(rb_iseq_t *iseq, VALUE *lfp)
{
    NODE *cref;
    if ((cref = lfp_get_special_cref(lfp)) != 0) {
	/* */
    }
    else if ((cref = iseq->cref_stack) != 0) {
	/* */
    }
    else {
	rb_bug("get_cref: unreachable");
    }
    return cref;
}

(vm.c)

lfp_get_special_cref() で cref が返ればそれを使い、そうでなければ iseq->cref_stack を使うようになっている。
lfp_get_special_cref() が cref を返す条件はまだ調べてないけれど、普通にクラスやメソッドの定義をしている場合には NULL を返すようだ。つまり、そういう場合には iseq->cref_stack が使われる。

iseq は YARV機械語の命令列を表わす構造体へのポインタだ。
YARVコンパイラは異なるスコープに属する命令列を分けて扱う。例えば、以下のようなコードをコンパイルすると main, クラス A, メソッド m の3つのスコープごとに命令列がそれぞれ生成される。

class A
  @@a = 1
  def m   
    p "a"
  end
end
p "b"   

YARVコンパイルした結果が以下の通り。説明は省くが、命令列が上記の3つに分かれているのが分かると思う。

== disasm: @->==============================================
local scope table (size: 1, argc: 0)

0000 putnil           
0001 putnil           
0002 defineclass      :A, , 0                                (   1)
0006 pop              
0007 putnil                                                           (   9)
0008 putstring        "b"
0010 send             :p, 1, nil, 8, 
0016 leave            
== disasm: @->===========================================
local scope table (size: 1, argc: 0)

0000 putobject        1                                               (   3)
0002 setclassvariable :@@a
0004 putnil                                                           (   4)
0005 definemethod     :m, m, 0
0009 putnil           
0010 leave            
== disasm: ===================================================
local scope table (size: 1, argc: 0)

0000 putnil                                                           (   5)
0001 putstring        "a"
0003 send             :p, 1, nil, 8, 
0009 leave            

さて、iseq に戻る。iseq はスコープごとにまとめられた命令列を持つため、スコープに関する情報も構造体の中に合わせて持っている。cref_stack はその一つだ。*1

cref_stack はクラスのネスト情報を表わすリンクリストだ。リストの先頭は一番下のレベルのクラス/モジュールを表わし、リストをたぐっていくにつれ、ネストの上位のクラス/モジュールが現れる。

get_cref() では iseq->cref_stack を使っているので、もっとも下のネストのクラスを返していることが分かる。

以上をまとめると以下のようになる。eval_get_cvar_base() は命令列を表わす iseq とスレッド th を引数に取って、それらが属するクラススコープのクラスを返す。
スレッドを引数に取るものの、今回見た例では th は使われない。つまり、iseq が属するクラススコープのクラスが返っていることになる。

eval_get_cvar_base() という名前から察するに、この関数は本来クラス変数の属するクラスを取ってくるのに使われるようだ。この関数が使われている他の場所を調べてみると、確かにクラス変数のアクセスのところで使われている。

結局、クラスローカルインスタンス変数の定義に使われるクラスは、クラス変数が属するクラスを探すのと同じロジックで探されているということになる。

*1:言い切ってはいるものの、rb_iseq_t 内の他のデータについては調べてないのでよく知らない。