第 14 章『コンテキスト』 ev_const_get() の解説

p.413 にある ev_const_get() の解説が完全に間違っていました。 小さな修正では回復不可能なため、以下に改めて解説をやりなおします。 ついでにおまけもつけました。

ev_const_get()

ev_const_get()

1550  static VALUE
1551  ev_const_get(cref, id, self)
1552      NODE *cref;
1553      ID id;
1554      VALUE self;
1555  {
1556      NODE *cbase = cref;
1557      VALUE result;
1558
1559      while (cbase && cbase->nd_next) {                                (A)
1560          VALUE klass = cbase->nd_clss;
1561
1562          if (NIL_P(klass)) return rb_const_get(CLASS_OF(self), id);   (B)
1563          if (RCLASS(klass)->iv_tbl &&
                  st_lookup(RCLASS(klass)->iv_tbl, id, &result)) {
1564              return result;
1565          }
1566          cbase = cbase->nd_next;
1567      }
1568      return rb_const_get(cref->nd_clss, id);                          (C)
1569  }

(eval.c)

引数のcref、つまり元はruby_cref、は NODEを利用したリンクリストであった。そのリストを 辿りながら直接st_lookup()iv_tblを調べる。 外のクラスのスーパークラスまでは調べないのが仕様だから、 rb_const_get()ではいけない。

書籍では「klassnilになったら」 トップレベルを越えたと説明している。つまり(B)のところが境界条件だと 説明している。だが正しくはトップレベルを越えたかどうかの判定をしているのは (A)の条件式である。

同時に、書籍ではスーパークラス方向の探索は(B)のreturn .... のところで行われていると説明したのだが、実際には(C)の rb_const_get()である。ということは探索の起点も selfではなくcref->nd_clssが正しい。

またここでwhileの条件が 「cbase && cbase->nd_clss」なのだから、 リストの最後にあるCREFに対してはループ本体が実行されない。 結果として、外のクラスを見るときにはトップレベルのクラスである Objectがスキップされる。p74、第 1 章『Ruby言語ミニマム』の 図1.11では外のクラスを探索するときにObjectの定数テーブルを 検索すると書いてあるのだが、これも間違いである。以下に同図を示す。

(wrong)       正(right)

コード追跡

ところで、トップレベルを越えたかどうかの判定がwhileで 済んでいるのなら、(B)のコードは何のためにあるのだろうか。 CREFnd_clssnilに なるというのはどういう状況だろう。

とりあえずは愚直にruby_crefへのアクセスを解析して…… みようかと思ったが、ちょっと試したらあまりに面倒そうなのでやめた。 方針を変えてcvs annotateしてみる。

~/src/ruby % cvs ann eval.c
                           :
                           :
1.1          (matz     16-Jan-98): static VALUE
1.163        (matz     13-Mar-01): ev_const_get(cref, id, self)
1.1          (matz     16-Jan-98):     NODE *cref;
1.1          (matz     16-Jan-98):     ID id;
1.163        (matz     13-Mar-01):     VALUE self;
1.1          (matz     16-Jan-98): {
1.1          (matz     16-Jan-98):     NODE *cbase = cref;
1.1          (matz     16-Jan-98):     VALUE result;
1.1          (matz     16-Jan-98): 
1.170        (matz     26-Mar-01):     while (cbase && cbase->nd_next) {
1.270        (matz     08-Mar-02):         VALUE klass = cbase->nd_clss;
1.1          (matz     16-Jan-98): 
1.163        (matz     13-Mar-01):         if (NIL_P(klass)) return rb_const_get(CLASS_OF(self), id);
1.270        (matz     08-Mar-02):         if (RCLASS(klass)->iv_tbl && st_lookup(RCLASS(klass)->iv_tbl, id, &result)) {
1.1          (matz     16-Jan-98):             return result;
1.1          (matz     16-Jan-98):         }
1.1          (matz     16-Jan-98):         cbase = cbase->nd_next;
1.1          (matz     16-Jan-98):     }
1.23         (matz     14-Dec-99):     return rb_const_get(cref->nd_clss, id);
1.17         (matz     17-Nov-99): }
                           :
                           :

非常にありがたいことに、問題の行が単独のリビジョンで変更されている。 今度はcvs diffで問題のリビジョンを追う。

~/src/ruby % cvs di -r1.162 -r1.163 eval.c
Index: eval.c
===================================================================
RCS file: /home/aamine/cvs/ruby/ruby/eval.c,v
retrieving revision 1.162
retrieving revision 1.163
diff -u -p -r1.162 -r1.163
--- eval.c      13 Mar 2001 05:45:08 -0000      1.162
+++ eval.c      13 Mar 2001 09:00:01 -0000      1.163
                     :
                     :
@@ -1441,9 +1442,10 @@ ev_const_defined(cref, id)
 }
 
 static VALUE
-ev_const_get(cref, id)
+ev_const_get(cref, id, self)
     NODE *cref;
     ID id;
+    VALUE self;
 {
     NODE *cbase = cref;
     VALUE result;
@@ -1451,7 +1453,7 @@ ev_const_get(cref, id)
     while (cbase && cbase->nd_clss != rb_cObject) {
        struct RClass *klass = RCLASS(cbase->nd_clss);
 
-       if (NIL_P(klass)) return rb_const_get(rb_cObject, id);
+       if (NIL_P(klass)) return rb_const_get(CLASS_OF(self), id);
        if (klass->iv_tbl && st_lookup(klass->iv_tbl, id, &result)) {
            return result;
        }

引数の変更だけだった。今気になっているのは条件のNIL_P(klass)が 加わった時期のほうなので、これでは意味がない。リビジョン1.162以前という 条件を付けて cvs annotatecvs diffでさらに探すことにする。 方法はオプション以外は先程と同じだ。

~/src/ruby % cvs ann -r1.162 eval.c
(略)
~/src/ruby % cvs diff -r1.158 -r1.159 eval.c
Index: eval.c
===================================================================
RCS file: /home/aamine/cvs/ruby/ruby/eval.c,v
retrieving revision 1.158
retrieving revision 1.159
diff -u -p -r1.158 -r1.159
--- eval.c      27 Feb 2001 07:52:11 -0000      1.158
+++ eval.c      28 Feb 2001 06:30:03 -0000      1.159
                    :
                    :
@@ -1451,6 +1451,7 @@ ev_const_get(cref, id)
     while (cbase && cbase->nd_clss != rb_cObject) {
        struct RClass *klass = RCLASS(cbase->nd_clss);
 
+       if (NIL_P(klass)) return rb_const_get(rb_cObject, id);
        if (klass->iv_tbl && st_lookup(klass->iv_tbl, id, &result)) {
            return result;
        }

おもいきりヒットした。こんなにうまくヒットするのは結構珍しいことだ。 今度は該当する日付のChangeLogを見る。 CVS(などソースコード管理システム)のログを付けているプロジェクトなら ログを見ればいいのだが、rubyではログは別のファイルで 管理している(ただし最近は同じ内容のログを両方に入れるようになった)。

Tue Feb 27 16:38:15 2001  Yukihiro Matsumoto  

        * eval.c (ev_const_get): retrieve Object's constant if no current
          class is available (e.g. defining singleton class for Fixnums).

        * eval.c (ev_const_defined): check Object's constant if no current
          class is available (e.g. defining singleton class for Fixnums).

        * time.c (time_timeval): negative time interval shoule not be
          allowed.

        * eval.c (proc_call): ignore block to `call' always, despite of
          being orphan or not.


(ChangeLog)

少し日付がずれていたが、うまく発見できた。嬉しいことに具体例付きだ。 Fixnumの特異クラスを定義しているときに ruby_crefがなくなるらしい。

しかし確か今はFixnumに特異メソッドは定義できないはずである。 試してみよう。

~ % ruby -ve 'def (1).m() end'
-e:1: can't define single method for literals
def (1).m() end
       ^

よく覚えていないのだが、定義できた時期があったような気もする。 とりあえず先程調べた2001-02-26ごろのバージョンをコンパイルして 確かめてみた。……と一言で言っているが、 当時と比べるとautoconfがバージョンアップしているため configureを作るのに少し苦労してしまった。

~/src/ruby-20010226 % ./ruby -v -e 'def (1).m() end'
ruby 1.7.0 (2001-02-26) [i686-linux]
-e:1: can't define single method for literals
def (1).m() end
       ^

……おかしい。当時もだめだったようだ。考えを変える必要がありそうだ。

方向転換して、メーリングリストのそのころのログを眺めてみることにする。 するとうまいこと関係しそうな話題を発見できた。

2001-02-27 [ruby-dev:12323] Re: [ruby-list:28364] class definition extention

からのスレッドである。なぜこれが気になったかと言うと、スレッドの途中で 特異クラスの話が出ていたからだ。このスレッドを元のruby-listにまで さかのぼって読んでみると、 「Fixnumに対するinstance_evalのブロックの中から 定数を参照すると落ちる」という記述を発見できた (instance_evalについては第 17 章『動的評価』を参照)。 例の行はこの問題に対する修正に違いない。再び検証する。

~/src/ruby-20010226 % ./ruby -v -e '1.instance_eval { p Object }'
ruby 1.7.0 (2001-02-26) [i686-linux]
-e:1: [BUG] Segmentation fault


~/src/ruby-20010226 %   ruby -v -e '1.instance_eval { p Object }'
ruby 1.7.3 (2002-09-11) [i686-linux]
Object

まだパッチのあたっていない旧バージョンは落ち、現在のバージョンは落ちない。 これで原因はほぼ確定した。あとはソースコード上で裏を取ればいい。 これはまあ地道にコードをたどるなり、デバッガで見るなりするだけだ。 それで結局のところは、 rb_obj_instance_eval()specific_eval()Qnilを渡しているのがそもそもの原因であった。これが exec_under()CREFに積まれ、 ev_const_getで出てくる。

これで解決である。

……ごめん、また嘘

ちゃんとソースコードにあたってみた方はいらっしゃるだろうか。 あるいはデバッガで見てみた方は。ちゃんと確認した真面目なあなたは 「instance_evalの例を実行しても、 現在のコードだと問題のコードは関係ない」 という事実に気付いてしまったはずである。なぜかというと、 そもそも無駄なCREFを積まないように修正されてしまったからだ。 以下は新旧のexec_under()のコードの一部である。

/* 旧バージョン */
exec_under(func, under, args)
    VALUE (*func)();
    VALUE under;
    void *args;
{
         :
         :
    if (ruby_cbase != under) {
        ruby_frame->cbase = (VALUE)rb_node_newnode(NODE_CREF,under,0,ruby_frame->cbase);
    }


/* 新バージョン */
static VALUE
exec_under(func, under, cbase, args)
    VALUE (*func)();
    VALUE under, cbase;
    void *args;
{
         :
         :
    if (cbase) {
        if (ruby_cbase != cbase) {
            ruby_frame->cbase = (VALUE)rb_node_newnode(NODE_CREF,under,0,ruby_frame->cbase);
        }
        PUSH_CREF(cbase);
    }

旧バージョンではunder = QnilCREFとして積まれる。 だが新バージョンではunderとは別にcbaseというのを 指定できるようになっており、 instance_evalのときはこれが0になるため、 CREFが積まれないのである。

ということで結論。 ev_const_get()if (NIL_P(klass))が真になる 条件は……「わからない」。変更の履歴から考えると「絶対に通らない」が 答えのような気はするのだが、どうにもeval.cは複雑すぎて 確信が持てない。 真面目に調べるには最初の発想通り几帳面にruby_crefへの アクセスを解析するしかないだろうが、面倒さに比べてあまり報われそうにない。 このあたりで止めておくことにしよう。


Copyright (c) 2002-2006 Minero Aoki, All rights reserved.