メソッドやクラス、変数などインタプリタの中にはいろいろなところで
名前を扱う必要がある。だが名前を文字列で持っているとそれだけでも
メモリを食うし、生成・解放がつきまとうので非常に扱うのが面倒になる。
それで Ruby の内部では名前を扱う時には文字列ではなく文字列と一対一に
対応する「ID」を使う。
ID は unsigned int (ruby.h) で、文字列から ID への変換は parse.y に
ある rb_intern() で行う。やっていることはわりと単純で、Ruby で書けば
次のようなコードで表せる。
@sym_tbl = {}
@last_id = 0
def rb_intern( str )
id = @sym_tbl[str] and return id
@last_id += 1
@sym_tbl[str] = @last_id
@last_id
end
実際には @sym_tbl はファイルスタティック、@last_id は関数スタティック
の変数だ。
基本はこうなのだが、一部の文字列、たとえば $ や @ が先頭につく文字列は
各所でそれと判別する必要が出てくるので、特別に高速化の工夫がされている。
ID の下位 3 ビット(8 通り)がそのためのフラグに使われていて、その内訳は
以下のよう。
#define ID_LOCAL 0x01
#define ID_INSTANCE 0x02
#define ID_GLOBAL 0x03
#define ID_ATTRSET 0x04
#define ID_CONST 0x05
(parse.y)
ID_ATTRSET は「page=」のように最後がイコールになっている文字列だ。
このタイプの ID は、最後の「=」を除いた文字列の ID に ID_ATTRSET を
セットしたものになる。ようするに、「any」の IDと「any=」の ID は
機械的に変換できる(rb_id_attrset)。
また、演算子 ("+"や"-") は特別扱いで、yaccが
決めた定数になる。yacc が決める定数は、一文字のシンボルに対しては
その文字コードそのままで、その他のシンボルには 256 から登場順に振られる。
これを利用して、実際には使われないトークン LAST_TOKEN を最後に定義して
おき、Ruby が独自に決める ID はその次の値から使うようになっている。
ちなみに ID は Ruby レベルでは Symbol で表されて、シンボルは :symbol や
'symbol'.intern で得られるのだった。intern のほうは string.c にあるので
ついでに見てみよう。
static VALUE
rb_str_intern(str)
VALUE str;
{
ID id;
if (strlen(RSTRING(str)->ptr) != RSTRING(str)->len)
rb_raise(rb_eArgError, "string contains `\\0'");
id = rb_intern(RSTRING(str)->ptr);
return ID2SYM(id);
}
(string.c)
ID2SYM は int を Symbol に変換するマクロ。1.4 以前はこれが INT2FIX
だった。Symbol も特殊なオブジェクト形式をしているのだが、その詳細に
ついてはオブジェクトの章を参照のこと。
インタプリタでは名前が多く出てくるが、名前があるならそれに対応する値も
なくてはいけない。その対応はいろいろな形式で表せるだろうが、Ruby では
これにハッシュを使っている。その実装が
st_table である。このライブラリの詳しい解析はしないが、
簡単なリファレンスだけつけておく。
int st_insert(st_table *table, char *key, char *value)
ハッシュに key と value の組を追加する。
古いライブラリなので void* のかわりに char* を使っている。
void st_add_direct(st_table *table, char *key, char *value)
st_insert() と似ているが、同じハッシュ値を持つエントリーに対する
「同値検査」を省略する。key がまだ登録されていないことがはっきり
している場合には、少し高速に登録できる。
int st_lookup(st_table *table, char *key, char **value)
key に対応する値をみつけて value にポインタを書きこむ。
返り値は見つかったかどうかの真偽値。
int st_is_member(st_table *table, char *key)
key が table に登録されているかどうか調べる。
void st_foreach(st_table table, enum st_retval (*func)(), char *arg)
Hash#each,delete_if などの実体。ハッシュ内の全てのキーと値、arg を
引数にして、func を実行する。func の返り値 enum st_retval は ST_CONTINUE
ST_STOP ST_DELETE のどれか。どれも見ためどおりの働きをする。
int st_delete(st_table *table, char **key, char **value),
*key に対応する値をテーブルから削除し、*key、*value に登録時のキーと
値を書きこむ。返り値は削除したかどうか。
int st_delete_safe(st_table *table, char **key, char **value, char *never)
st_delete() と似ているが、その場ですぐに削除するのではなく never を
書きこんでおく。st_cleanup_safe() で本当に削除できる。
Ruby では never には Qundef を使う。
void st_cleanup_safe(st_table table, char *never)
never と同じ値を持つエントリーを削除する。
st_table *st_init_table(struct st_hash_type type), ..._with_size()
st_table を作成する。_with_size はサイズを指定して生成する。
struct st_hash_type はハッシュ値を得る関数と、同値判定を行う関数を持つ
st_table *st_init_numtable(), ..._with_size()
int のハッシュを作成する。
st_init_table() に int 用の操作関数を渡しているだけ。
st_table *st_init_strtable(), ..._with_size()
文字列用のハッシュを作成する。
st_init_table() に文字列用の操作関数を渡しているだけ。
void st_free_table(st_table *table)
table を解放する。キー、値は解放されない。
st_table *st_copy(st_table *old_table)
Hash#dup の実体。
old_table と同じ内容の st_table を新たに作成して返す。