p.413 にある 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()ではいけない。
書籍では「klassがnilになったら」
トップレベルを越えたと説明している。つまり(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の定数テーブルを
検索すると書いてあるのだが、これも間違いである。以下に同図を示す。
誤
正
ところで、トップレベルを越えたかどうかの判定がwhileで
済んでいるのなら、(B)のコードは何のためにあるのだろうか。
CREFのnd_clssがnilに
なるというのはどういう状況だろう。
とりあえずは愚直に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 annotateとcvs 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 = QnilがCREFとして積まれる。
だが新バージョンでは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.