p.413 にある 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.