第13章 評価器の構造

第四部の概略

評価器とは

「評価器」なんて聞き慣れない言葉だ。評価器という字面から考えるに 「評価」 するための「器械」には違いないけども、それでは評価するとはどういうこと だろうか。

「評価」はevaluateの定訳だが、プログラム言語について話すという前提では、 誤訳ぎみだ。日本語で評価と言うとどうしても「良い悪い」の意味合いが含ま れてしまう。

プログラム言語におけるevaluateは良し悪しとは全く関係なく、「推し測る」 とか「実行する」に近い意味を持つ。evaluateの語源はラテン語の ex+value+ateで、直訳すれば「値にする」となる。これが一番理解しやすいか もしれない。テキストで表現された式から値を求める、というわけだ。

まあようするにぶっちゃけて言えば評価とは書かれている式を実行してその結 果を得る、という意味である。ではなぜ実行と言わないかと言うと、実行だけ が評価ではないからだ。

例えば普通のプログラム言語なら「3」と書けば整数の3として扱われる。こう いうとき「"3"を評価した結果は3である」と言ったりする。定数の式は実行し ているとは言い難いが、それもやはり評価なのである。「3」という字面を 評価したときに整数6として扱われる(評価される)言語があっても、別に構 わないのだから。

別の例を挙げよう。定数を組み合わせた式があると、コンパイル中に計算して しまうことがある(定数畳み込み)。これも普通「実行」して計算していると は言わない。実行と言えば作ったバイナリが動いている間の作業を指すからだ。 しかしいつ計算しようとプログラムの最終的な評価値は変わらない。

つまり「評価する」とはたいていはプログラムを実行することと等しいのだが、 根本的には実行と評価は違う。とりあえずこの点だけ覚えておいてほしい。

rubyの評価器の特徴

rubyの評価器で最大の特徴は、インタプリタ全体に言えることだが、Cレベル (拡張ライブラリ)とRubyレベルのコードの表現の差が小さいことだ。普通の 処理系では拡張ライブラリから使えるインタプリタの機能はかなり制限される ものだが、rubyでは恐ろしく制限が少ない。クラス定義、メソッド定義、制限 無しのメソッド呼び出し、あたりは当然として、例外処理、イテレータ、果て はスレッドまで使えてしまう。

しかしその便利さの代償はどこかで払わなければならない。実装が異様に大変 だったり、オーバーヘッドが大きかったり、同じようなことをRuby用とC用に 二回実装している個所もかなりある。

またrubyは動的な言語なので、実行時にプログラムを文字列で組み立てて それを評価させることもできる。evalという関数風メソッドがそれだ。 名前はもちろんevaluateから来ている。それを使うとこんなこともできる。

lvar = 1
answer = eval("lvar + lvar")    # 答えは2

その他にModule#module_evalObject#instance_evalというものもあり、 またそれぞれに癖のある動きをする。このあたりの詳しいことは 第17章『動的評価』で述べよう。

eval.c

評価器はeval.cで実装されている。ところがこのeval.cが非常に巨大で、実に 9000行200Kバイト、関数の数309という難敵である。このくらいになってくる と頭から眺めた程度ではさっぱり構造がつかめない。

ではどうするか。まず、巨大なファイルになればなるほど、その中に全く区分 けがないということは考えられない。つまり内部でもっと小さい単位にモジュー ル化されているはずなのだ。だからそれを探すことを第一に考えるべきである。 ではモジュールをどうやって探せばいいだろう。 方法をいくつか羅列してみる。

まず定義されている関数のリストを出力してプリフィクスを見る。rb_dvar_rb_mod_rb_obj_rb_thread_といったプリフィクスを持った関数がズラズラ と並んでいるはずだ。これは明らかに同種の関数のまとまりである。

またクラスライブラリのコードを見るとわかるのだが、rubyのコードでは Init_xxxx()は常にブロックの最後に置かれる。だからInit_xxxx()があったら そこも切れめである。

それに当然ながら名前も重要である。eval()rb_eval()eval_node()が 近くにあったら互いに深い関係があると思うのが当然だ。

最後に、rubyのソースコードでは型や変数の定義、プロトタイプ宣言がはさまっ ていると切れめであることが多い。

このあたりを意識して見てみると、eval.cは主に以下のようなモジュールに 分割できそうだ。

セーフレベル第7章『セキュリティ』で説明済み
メソッドエントリの操作メソッド実体の構文木の探索や削除を行う
評価器コアrb_eval()を中心とした、評価器の核心部
例外例外の発生とバックトレース生成
メソッドメソッド呼び出しの実装
イテレータブロックに関係する関数の実装
ロード外部ファイルのロードと評価
ProcProcの実装
スレッドRubyスレッドの実装

このうちロードとスレッドのパートは本来eval.cにあるべきではない部分だ。 eval.cにあるのは単にC言語の制約が理由である。もう少し言うと、 PUSH_TAG()などeval.cの内部で定義されているマクロを使う必要が あるからだ。そこでこの二つは第三部から外し、第四部で扱うことにした。 またセーフレベルについては第一部で説明してしまったのでもういいだろう。

以上の三つを削った残りの六項目が第三部の解説対象である。 章との対応は以下の表のようになる。

▼第三部解説割り振り

メソッドエントリの操作次章『コンテキスト』
評価器コア第三部全章
例外本章
メソッド第15章『メソッド』
イテレータ第16章『ブロック』
Proc第16章『ブロック』

mainruby_run経由rb_eval行き

コールグラフ

評価器の真のコアはrb_eval()という関数なのだが、 この節はmain()からそのrb_eval()までの道筋を追ってゆく。 まず以下はrb_eval()周辺のおおまかなコールグラフである。

main                     ....main.c
    ruby_init                ....eval.c
        ruby_prog_init           ....ruby.c
    ruby_options             ....eval.c
        ruby_process_options     ....ruby.c
    ruby_run                 ....eval.c
        eval_node
            rb_eval
                *
        ruby_stop

右側にはファイルが移動するところでファイル名を入れておいた。 これをジッと見てみてまず気付くのは、eval.cの関数からmain.cの 関数を呼び返していることだ。

呼び返している、と書いたのは、main.cruby.cはどちらかというと rubyコマンドの実装となるファイルだからだ。eval.crubyコマンドとは 少し距離を置いた、評価器自体の実装である。つまりeval.cruby.cに 使われる立場であって、eval.cからruby.cの関数を呼んでしまったら eval.cの独立性が下がる。

ではどうしてこうなっているのだろう。理由は主にC言語の制約による。 ruby_prog_init()ruby_process_options()ではRuby世界のAPIを使い はじめるので、例外が起きる可能性がある。しかしRubyの例外を止める ためにはeval.cの中でしか使えないPUSH_TAG()というマクロを使わないと ならないのだ。つまり本来はruby_init()ruby_run()のほうがruby.cに あるべきだということである。

ではどうしてPUSH_TAG()extern関数なりなんなりにして公開しないのだろう。 実はPUSH_TAG()は次のようにPOP_TAG()と対にしないと使えないのだ。

PUSH_TAG();
/* いろいろする */
POP_TAG();

実装上の理由から、この二つのマクロは同じ関数に入れておかないといけない。 やろうと思えば別々の関数に分けられるように実装することもできるのだが、 そうすると速度が遅くなるのでそうはなっていないのだ。

次に気付くのは、main()からruby_xxxx()という名前の関数を連続で呼んで いるのはとても意味がありそうだ、ということである。これだけあからさまに 対称形なら何も関係ないほうがおかしい。

実際この三つの関数には深い関係がある。一言で言ってしまうと、この三つは どれも「組み込みRubyインターフェイス」なのだ。つまりrubyインタプリタを 組み込んだコマンドを作るときだけ使う、そして拡張ライブラリでは使わない、 関数なのだ。rubyコマンド自体も理論上は組み込みRubyプログラムの一種と考 えられるから、このインターフェイスを使うのが自然だ。

ruby_というプリフィクスはなんだろう。rubyの関数はこれまで全て rb_だった。どうしてrb_ruby_の二種類あるのだろうか。調べてみ てもどうにも違いがわからないので直接理由を聞いてみたところ、「rubyコ マンドの補助をするものがruby_で公式インターフェイスがrb_」なんだそ うだ。

じゃあeval.cの内部だけで使うruby_scopeなどはなぜruby_なのか? と ツッコんでみると、これは単なる偶然らしい。ruby_scopeなどは 元々the_という名前だったのだが、1.3の途中で全インターフェイスに プリフィクスを付ける変更が入った。そのときに「なんとなく内部っぽい」 変数にruby_を付けたんだそうな。

つまり結論だけ言うと、ruby_が付いているものはrubyコマンド補助用 または内部専用変数。rb_が付いているものはrubyインタプリタの公式 インターフェイス。ということだ。

main()

最初は素直にmain()から見ていくことにしよう。 ここは非常に短かくて助かる。

main()

  36  int
  37  main(argc, argv, envp)
  38      int argc;
  39      char **argv, **envp;
  40  {
  41  #if defined(NT)
  42      NtInitialize(&argc, &argv);
  43  #endif
  44  #if defined(__MACOS__) && defined(__MWERKS__)
  45      argc = ccommand(&argv);
  46  #endif
  47
  48      ruby_init();
  49      ruby_options(argc, argv);
  50      ruby_run();
  51      return 0;
  52  }

(main.c)

#ifdef NTは当然ながらWindows NTのNTである。が、なぜかWin9xでも NTは定義される。つまりWin32環境という意味だ。 NtInitialize()はWin32のためにargc argvとソケットシステム (WinSock)を初期化する。この関数は初期化をしているだけなので 面白くないし本筋にも関係ないので省略する。

また__MACOS__は「まこす」でなくてMac OSのこと。この場合Mac OS 9以前の ことで、Mac OS Xは入らない。こんな#ifdefは残っているが、最初に書いたと おり現在のバージョンはMac OS 9以前では動かない。動いていたころの名残で ある。よってこのコードも略。

ところでC言語に詳しい読者ならば知っていると思うが、アンダーバーで始ま る識別子はシステムライブラリやOSのために予約されている。もっとも予約さ れているとは言っても使うとエラーになったりするということはまずないが、 ちょっとヘンなccではエラーになることもある。例えばHP-UXのcc。 HP-UXというのはHPの作っているUNIXのことだ。HP-UXはヘンじゃない、 という意見は声を大にして却下する。

まあ、なんにしてもユーザアプリケーションではそのような識別子は定義しない のがお約束である。

では組み込みRubyインターフェイスをさらっと解説していこう。

ruby_init()

ruby_init()はRubyインタプリタを初期化する。現在のRubyインタプリタは一 つのプロセスに一つしか存在できないので引数も返り値も必要ない。この点は 一般には「機能不足」と見られている。

インタプリタが一つだけだと困るのはなんと言ってもRubyの開発環境まわりだ ろう。具体的にはirbであるとかRubyWin、RDEと言ったアプリケーションだ。 プログラムを書き直してロードしても消したはずのクラスが残ってしまう。リ フレクションAPIを駆使すれば対処できないこともないが、かなり苦しい。

だがまつもとさんはどうやら意図的にインタプリタの数を一つに制限している ようだ。「完全な初期化はできないから」というのが理由らしい。例えばロー ドしてしまった拡張ライブラリを外すことはできないというのが例として挙げ られている。

ruby_init()のコードは見ても意味がないので略。

ruby_options()

Rubyインタプリタのためにコマンドラインオプションをパースするのが ruby_options()である。もちろんコマンドによっては使わなくてもよい。

この関数の中で-r(ライブラリのロード)や -e(コマンドラインからプログラムを渡す)の処理は行われてしまう。 それに引数に渡したファイルをRubyプログラムとしてパースするのもここだ。

rubyコマンドはファイルを与えられればそこから、与えられなければ stdinから、メインプログラムを読む。そうしたら第二部で紹介した rb_compile_string()rb_compile_file()を使ってテキストを構文木に コンパイルする。その結果はグローバル変数ruby_eval_treeにセット される。

ruby_options()のコードも地道すぎて面白くないので略。

ruby_run()

そして最後にruby_run()ruby_eval_treeにセットされている構文木の 評価を開始する。この関数も必ずしも呼ぶ必要はない。ruby_run()以外には 例えばrb_eval_string()という関数を使って文字列を評価する方法がある。

ruby_run()

1257  void
1258  ruby_run()
1259  {
1260      int state;
1261      static int ex;
1262      volatile NODE *tmp;
1263
1264      if (ruby_nerrs > 0) exit(ruby_nerrs);
1265
1266      Init_stack((void*)&tmp);
1267      PUSH_TAG(PROT_NONE);
1268      PUSH_ITER(ITER_NOT);
1269      if ((state = EXEC_TAG()) == 0) {
1270          eval_node(ruby_top_self, ruby_eval_tree);
1271      }
1272      POP_ITER();
1273      POP_TAG();
1274
1275      if (state && !ex) ex = state;
1276      ruby_stop(ex);
1277  }

(eval.c)

PUSH_xxxx()というマクロが見えるが、とりあえず無視しておいていい。このあ たりの詳しいことはいずれときが来たら説明する。ここでは重要なのは eval_node()だけだ。その中身はと言うと、

eval_node()

1112  static VALUE
1113  eval_node(self, node)
1114      VALUE self;
1115      NODE *node;
1116  {
1117      NODE *beg_tree = ruby_eval_tree_begin;
1118
1119      ruby_eval_tree_begin = 0;
1120      if (beg_tree) {
1121          rb_eval(self, beg_tree);
1122      }
1123
1124      if (!node) return Qnil;
1125      return rb_eval(self, node);
1126  }

(eval.c)

ruby_eval_treeに対してrb_eval()を呼ぶ。ruby_eval_tree_beginのほうは BEGINで登録された文を格納している。が、これもどうでもいい。

またruby_run()の中のruby_stop()では、スレッドを全部終了させて、 オブジェクトを全てファイナライズし、例外をチェックして最終的に exit()を呼ぶ。こちらもどうでもいいので見ない。

rb_eval()

概要

さてrb_eval()である。この関数こそがrubyの真の核だ。 rb_eval()の呼び出し一回がNODE一つを処理し、再帰呼び出し しながら構文木全体を処理していく(図1)。

(rbeval)
図1: rb_evalのイメージ

rb_eval()yylex()と同じく巨大なswitch文でできており、 ノードごとに分岐するようになっている。まずは概形から見てみよう。

rb_eval()概形

2221  static VALUE
2222  rb_eval(self, n)
2223      VALUE self;
2224      NODE *n;
2225  {
2226      NODE *nodesave = ruby_current_node;
2227      NODE * volatile node = n;
2228      int state;
2229      volatile VALUE result = Qnil;
2230
2231  #define RETURN(v) do { \
2232      result = (v);      \
2233      goto finish;       \
2234  } while (0)
2235
2236    again:
2237      if (!node) RETURN(Qnil);
2238
2239      ruby_last_node = ruby_current_node = node;
2240      switch (nd_type(node)) {
            case NODE_BLOCK:
              .....
            case NODE_POSTEXE:
              .....
            case NODE_BEGIN:
                   :
            (大量のcase文)
                   :
3415        default:
3416          rb_bug("unknown node type %d", nd_type(node));
3417      }
3418    finish:
3419      CHECK_INTS;
3420      ruby_current_node = nodesave;
3421      return result;
3422  }

(eval.c)

省略したところには全ノードの処理コードがずらっと並んでいる。 こうやって分岐してノードごとの処理を行うわけだ。コードが少なければ rb_eval()の中だけで処理されてしまうが、多くなってくると関数に 分割される。eval.cの関数のほとんどはそうやってできたものだ。

rb_eval()から値を返すときはreturnでなくマクロRETURN()を使う。 必ずCHECK_INTSを通るようにするためである。このマクロはスレッド 関係なので、そのときまで無視しておいてよい。

それから最後に、ローカル変数のresultnodevolatileしているのはGC対策である。

NODE_IF

ではif文を例にとってrb_eval()による評価の過程を具体的に見てみよう。 以下、rb_eval()の解説では

の三つを冒頭に挙げて読んでいくことにする。

▼ソースプログラム

if true
  'true expr'
else
  'false expr'
end

▼対応する構文木(nodedump

NODE_NEWLINE
nd_file = "if"
nd_nth  = 1
nd_next:
    NODE_IF
    nd_cond:
        NODE_TRUE
    nd_body:
        NODE_NEWLINE
        nd_file = "if"
        nd_nth  = 2
        nd_next:
            NODE_STR
            nd_lit = "true expr":String
    nd_else:
        NODE_NEWLINE
        nd_file = "if"
        nd_nth  = 4
        nd_next:
            NODE_STR
            nd_lit = "false expr":String

第二部で見たとおり、elsifunlessは組みかたを工夫することで NODE_IF一種類にまとめられるので特別に扱う必要はない。

rb_eval()NODE_IF

2324  case NODE_IF:
2325    if (trace_func) {
2326        call_trace_func("line", node, self,
2327                        ruby_frame->last_func,
2328                        ruby_frame->last_class);
2329    }
2330    if (RTEST(rb_eval(self, node->nd_cond))) {
2331        node = node->nd_body;
2332    }
2333    else {
2334        node = node->nd_else;
2335    }
2336    goto again;

(eval.c)

重要なのは最後のif文だけである。 この文を意味を変えずに書きなおすとこうなる。

if (RTEST(rb_eval(self, node->nd_cond))) {     (A)
    RETURN(rb_eval(self, node->nd_body));      (B)
}
else {
    RETURN(rb_eval(self, node->nd_else));      (C)
}

まず(A)でRubyの条件式(のノード)を評価し、その値をRTEST()でテストする。 RTEST()VALUEがRubyの真であるかどうかテストするマクロだった。 それが真なら(B)でthen側の節を評価し、 偽なら(C)でelse側の節を評価する。

そしてRubyのif式もまた値を持つのだったから、値を返さないといけない。 ifの値はthen側またはelse側、実行されたほうの節の値だから、 RETURN()マクロでそれを返す。

オリジナルのリストでrb_eval()を再帰呼び出しせずにgotoで済ませてい るのは前章『構文木の構築』でも登場した「末尾再帰→goto変換」である。

NODE_NEWLINE

if式のところにNODE_NEWLINEがあったのでその処理コードも見ておこう。 このノードはstmt一つにつき一つつくヘッダのようなものであった。 rb_eval()では実行中のプログラムの、ファイル上での位置を 逐次合わせるために使われる。

rb_eval()NODE_NEWLINE

3404  case NODE_NEWLINE:
3405    ruby_sourcefile = node->nd_file;
3406    ruby_sourceline = node->nd_nth;
3407    if (trace_func) {
3408        call_trace_func("line", node, self,
3409                        ruby_frame->last_func,
3410                        ruby_frame->last_class);
3411    }
3412    node = node->nd_next;
3413    goto again;

(eval.c)

特に難しいことはない。

call_trace_func()NODE_IFでも登場していた。これがどういうものかと いうことだけ 簡単に解説しておこう。これはRubyプログラムをRubyレベルからトレースす るための機能である。デバッガ(debug.rb)やトレーサ(tracer.rb)、 プロファイラ(profile.rb)、 irb(対話的なrubyコマンド)などがこの機能を使っている。

set_trace_funcという関数風メソッドを呼ぶとトレース用のProcオブ ジェクトを登録できるようになっていて、そのProcオブジェクトが trace_funcに入っている。trace_funcが0でなかったら、つまり Qfalseでなかったら、それをProcオブジェクトと見做して (call_trace_func()で)実行する。

このcall_trace_func()は本筋とはまるで関係がないうえ、あまり面白くな いので本書では以後一切無視する。興味のある人は第16章『ブロック』を 読んでから挑戦してほしい。

擬似ローカル変数

NODE_IFなどは構文木で言えば節に当たる部分であった。 葉の部分も見ておこう。

rb_eval()−擬似ローカル変数ノード

2312  case NODE_SELF:
2313    RETURN(self);
2314
2315  case NODE_NIL:
2316    RETURN(Qnil);
2317
2318  case NODE_TRUE:
2319    RETURN(Qtrue);
2320
2321  case NODE_FALSE:
2322    RETURN(Qfalse);

(eval.c)

selfrb_eval()の引数だった。ちょっと前に戻って確認しておいてほしい。 他はいいだろう。

ジャンプタグ

次はwhileに対応するノードNODE_WHILEを解説していきたいのだが、 breaknextを実装するには関数の再帰呼び出しだけでは難しい。 rubyはこれらの構文をジャンプタグというものを使って実現しているので、 まずはその話をしよう。

ジャンプタグを一言で言うと、C言語のライブラリ関数setjmp()longjmp()の ラッパーである。setjmp()のことは知っているだろうか。 この関数はgc.cでも出てきたが、あっちでの使いかたはかなり邪道だ。 setjmp()は普通は関数越しジャンプのために使うものである。 以下のコードを例にして説明しよう。エントリポイントはparent()だ。

setjmp()longjmp()

jmp_buf buf;

void child2(void) {
    longjmp(buf, 34);   /* parentまで一気に戻る。
                           setjmpの返り値は34になる */
    puts("このメッセージは絶対に出力されない");
}

void child1(void) {
    child2();
    puts("このメッセージは絶対に出力されない");
}

void parent(void) {
    int result;
    if ((result = setjmp(buf)) == 0) {
        /* 普通にsetjmpから戻ってきた */
        child1();
    } else {
        /* child2からlongjmpで戻ってきた */
        printf("%d\n", result);   /* 34と表示される */
    }
}

まずparent()setjmp()を呼ぶと引数のbufにその時の実行状態が記録 される。もうちょっと直接的に言うと、マシンスタックの先端アドレスと CPUのレジスタが記録される。setjmp()の返り値が0だったらそのsetjmp()か ら普通に戻ってきたということなので、普通に続きのコードを書けばよい。そ れがif側だ。ここではchild1()を呼び出している。

そして次にchild2()に制御が移ってlongjmpを呼ぶと、引数のbufsetjmpした ところにいきなり戻ることができる。つまりこの場合はparent()setjmpのと ころまで戻る。longjmpで戻ったときはsetjmp()の返り値がlongjmpの第二 引数の値になるのでelse側が実行される。ちなみにlongjmpに0を渡しても 強制的に違う値にされてしまうので無駄だ。

例によってマシンスタックの状態を図にすると図2のようになる。 普通の関数は一回の呼び出しにつき一回しか戻らないものだ。しかし setjmp()は二回戻ることがある。fork()みたいなものだと言ったら少しは イメージがつかめるだろうか。

(setjmp)
図2: setjmp() longjmp()のイメージ

さて、setjmp()の予習はここまでだ。eval.cではEXEC_TAG()setjmp()JUMP_TAG()longjmp()に、それぞれ相当する(図3)。

(jumptag)
図3: タグジャンプのイメージ

この図を見るとEXEC_TAG()に引数がないようだ。jmp_bufは どこに行ってしまったのだろう。 実はrubyではjmp_bufstruct tagという構造体にラップされている。 見てみよう。

struct tag

 783  struct tag {
 784      jmp_buf buf;
 785      struct FRAME *frame;   /* PUSH_TAGしたときのFRAME */
 786      struct iter *iter;     /* PUSH_TAGしたときのITER */
 787      ID tag;                /* タグの種類 */
 788      VALUE retval;          /* ジャンプに伴う返り値 */
 789      struct SCOPE *scope;   /* PUSH_TAGしたときのSCOPE */
 790      int dst;               /* ジャンプ先ID */
 791      struct tag *prev;
 792  };

(eval.c)

prevメンバがあるのでstruct tagはリンクリストを使ったスタック 構造だろうと予測できる。さらにまわりを見回してみるとPUSH_TAG()POP_TAG()というマクロがあるので、スタックで間違いなさそうだ。

PUSH_TAG() POP_TAG()

 793  static struct tag *prot_tag;   /* スタックの先端を指すポインタ */

 795  #define PUSH_TAG(ptag) do {             \
 796      struct tag _tag;                    \
 797      _tag.retval = Qnil;                 \
 798      _tag.frame = ruby_frame;            \
 799      _tag.iter = ruby_iter;              \
 800      _tag.prev = prot_tag;               \
 801      _tag.scope = ruby_scope;            \
 802      _tag.tag = ptag;                    \
 803      _tag.dst = 0;                       \
 804      prot_tag = &_tag

 818  #define POP_TAG()                       \
 819      if (_tag.prev)                      \
 820          _tag.prev->retval = _tag.retval;\
 821      prot_tag = _tag.prev;               \
 822  } while (0)

(eval.c)

ここで唖然としてほしいのだが、なんとタグの実体はローカル変数としてマシ ンスタックにベタ置き確保されているようだ(図4)。しかも dowhileが二つのマクロに分離している。Cのプリプロセッサの使いかた でもこれはかなり凶悪な部類に入るのではなかろうか。わかりやすく PUSH/POPのマクロを組にして展開するとこうなる。

do {
    struct tag _tag;
    _tag.prev = prot_tag;   /* 前のタグを保存 */
    prot_tag = &_tag;       /* 新しいタグをスタックに積む */
    /* いろいろする */
    prot_tag = _tag.prev;   /* 前のタグを復帰 */
} while (0);

この方法だと関数呼び出しのオーバーヘッドもなければメモリ割り当てのコス トもないに等しい。最もこの手法はrubyの評価器がrb_eval()の再帰でできて いるからこそできる技だ。

(tagstack)
図4: タグスタックはマシンスタックに埋め込まれている

こういう実装なのでPUSH_TAG()POP_TAG()は一つの関数に対にして 置かないとまずい。またうっかり評価器の外で使われてよいものでもない ので公開もできない。

ついでにEXEC_TAG()JUMP_TAG()も見ておこう。

EXEC_TAG() JUMP_TAG()

 810  #define EXEC_TAG()    setjmp(prot_tag->buf)

 812  #define JUMP_TAG(st) do {               \
 813      ruby_frame = prot_tag->frame;       \
 814      ruby_iter = prot_tag->iter;         \
 815      longjmp(prot_tag->buf,(st));        \
 816  } while (0)

(eval.c)

このように、setjmplongjmpがそれぞれEXEC_TAG()JUMP_TAG()に ラップされている。EXEC_TAG()という字面だけ見ると一瞬longjmp()の ラッパーのように見えるのだが、こちらがsetjmp()の実行である。

以上を踏まえてwhileの仕組みを説明しよう。まずwhileの開始時に EXEC_TAG()する(setjmp)。その後rb_eval()を再帰して本体を実行する。 そしてbreaknextがあるとJUMP_TAG()する(longjmp)。 するとwhileループの開始地点まで戻れるというわけだ(図5)。

(whilejmp)
図5: タグジャンプによるwhileの実装

ここではbreakを例にしたが、ジャンプしないと実装できないのはbreakだ けではない。whileに限ってもnextredoがある。またメソッドからの returnや例外でもrb_eval()の壁を越えなければいけないはずだ。そして それぞれのために別のタグスタックを使うのは面倒だから、なんとかしてスタッ ク一本でまとめたいものだ。

それを実現するには、「このジャンプは何のためのジャンプか」という情報を つければいい。都合のいいことにlongjmp()に引数を渡すとsetjmp()の返 り値を指定することができたから、それを使えばよさそうだ。その種類が次の フラグで示されている。

▼タグタイプ

 828  #define TAG_RETURN      0x1    /* return */
 829  #define TAG_BREAK       0x2    /* break */
 830  #define TAG_NEXT        0x3    /* next */
 831  #define TAG_RETRY       0x4    /* retry */
 832  #define TAG_REDO        0x5    /* redo */
 833  #define TAG_RAISE       0x6    /* 一般の例外 */
 834  #define TAG_THROW       0x7    /* throw(本書では扱わない)*/
 835  #define TAG_FATAL       0x8    /* 補足不可能な例外fatal */
 836  #define TAG_MASK        0xf

(eval.c)

意味はコメントの通り。最後のTAG_MASKsetjmp()の返り値からこれらの フラグを取り出すためのビットマスクである。setjmp()の返り値には「ジャ ンプの種類」以外の情報も載せることがあるからだ。

NODE_WHILE

ではNODE_WHILEのコードで実際のタグの使いかたを確かめよう。

▼ソースプログラム

while true
  'true_expr'
end

▼対応する構文木(nodedump-short

NODE_WHILE
nd_state = 1 (while)
nd_cond:
    NODE_TRUE
nd_body:
    NODE_STR
    nd_lit = "true_expr":String

rb_evalNODE_WHILE

2418  case NODE_WHILE:
2419    PUSH_TAG(PROT_NONE);
2420    result = Qnil;
2421    switch (state = EXEC_TAG()) {
2422      case 0:
2423        if (node->nd_state && !RTEST(rb_eval(self, node->nd_cond)))
2424            goto while_out;
2425        do {
2426          while_redo:
2427            rb_eval(self, node->nd_body);
2428          while_next:
2429            ;
2430        } while (RTEST(rb_eval(self, node->nd_cond)));
2431        break;
2432
2433      case TAG_REDO:
2434        state = 0;
2435        goto while_redo;
2436      case TAG_NEXT:
2437        state = 0;
2438        goto while_next;
2439      case TAG_BREAK:
2440        state = 0;
2441        result = prot_tag->retval;
2442      default:
2443        break;
2444    }
2445  while_out:
2446    POP_TAG();
2447    if (state) JUMP_TAG(state);
2448    RETURN(result);

(eval.c)

ここにはこれから何度も何度も登場するイディオムが登場している。

PUSH_TAG(PROT_NONE);
switch (state = EXEC_TAG()) {
  case 0:
    /* 通常の処理を行う */
    break;
  case TAG_a:
    state = 0;    /* 自分が待っていたジャンプだからstateをクリア */
    /* TAG_aで飛んできたときの処理 */
    break;
  case TAG_b:
    state = 0;    /* 自分が待っていたジャンプだからstateをクリア */
    /* TAG_bで飛んできたときの処理 */
    break;
  default
    break;        /* 自分が待っていたジャンプではない。すると…… */
}
POP_TAG();
if (state) JUMP_TAG(state);   /* ……ここで再ジャンプする。*/

まずPUSH_TAG()POP_TAG()は前述のような仕組みなので必ず対にしなければ ならない。またEXEC_TAG()よりも外側になければならない。そしていま積んだ ばかりのjmp_bufEXEC_TAG()する。つまりsetjmp()する。返り値が0なら setjmp()からすぐに戻ってきたということなので通常の処理を行う(普通は rb_eval()を含む)。EXEC_TAG()の返り値が0以外ならlongjmp()で戻ってきた ということなので自分が必要なジャンプだけcaseで漉し取り、残りは通す (default)。

ジャンプするほうのコードも合わせて見るとわかりやすいかもしれない。 以下にredoのノードのハンドラを示す。

rb_eval()NODE_REDO

2560  case NODE_REDO:
2561    CHECK_INTS;
2562    JUMP_TAG(TAG_REDO);
2563    break;

(eval.c)

JUMP_TAG()で飛ぶと、その一つ前に行ったEXEC_TAG()に戻る。 その時の返り値は引数のTAG_REDOだ。それを考えつつNODE_WHILEの コードを眺めて、どういう経路を通るか確認してほしい。

ではイディオムはいいとして、NODE_WHILEのコードをもうちょっと詳しく 説明する。言った通りcase 0:の中が本処理なのでそこだけ取りだし、 ついでに読みやすくなるようにラベルをいくつかずらしてみた。

  if (node->nd_state && !RTEST(rb_eval(self, node->nd_cond)))
      goto while_out;
  do {
      rb_eval(self, node->nd_body);
  } while (RTEST(rb_eval(self, node->nd_cond)));
while_out:

条件式に相当するnode->nd_condが二ヶ所でrb_eval()されている。一回目 の条件判定だけが分離されているようだ。これはdowhilewhileをま とめて扱うためである。node->nd_stateが0のときがdowhile、1のと きが普通のwhileだ。あとは地道に追ってもらえればわかるだろう。いちい ち説明はしない。

ところで、もし条件式の中にnextredoがあったら簡単に 無限ループになりそうな気がする。もちろんそういう意味の コードなのだから書くほうが悪いのだが、ちょっと気になる。 そこで実際にやってみた。

% ruby -e 'while next do nil end'
-e:1: void value expression

あっさりパースの時点でハネられてしまった。安全ではあるが、 面白い結果ではない。ちなみにこのエラーを出して いるのがparse.yvalue_expr()である。

whileの評価値

長いことwhileは値を持たなかったのだが、ruby 1.7からはbreakで値を返せる ようになった。今度は評価値の流れに注目してみよう。ローカル変数resultの 値がrb_eval()の返り値になることを頭に入れて見てほしい。

        result = Qnil;
        switch (state = EXEC_TAG()) {
          case 0:
            /* 本処理 */
          case TAG_REDO:
          case TAG_NEXT:
            /* それぞれジャンプ */

          case TAG_BREAK:
            state = 0;
            result = prot_tag->retval;     (A)
          default:
            break;
        }
        RETURN(result);

注目すべきは(A)のみである。ジャンプの返り値は prot_tag->retvalつまりstruct tagを経由して渡されているようだ。 渡すほうはこうだ。

rb_eval()NODE_BREAK

2219  #define return_value(v) prot_tag->retval = (v)

2539  case NODE_BREAK:
2540    if (node->nd_stts) {
2541        return_value(avalue_to_svalue(rb_eval(self, node->nd_stts)));
2542    }
2543    else {
2544        return_value(Qnil);
2545    }
2546    JUMP_TAG(TAG_BREAK);
2547    break;

(eval.c)

このようにreturn_value()というマクロでタグスタック先端の構造体に 値を入れておく。

基本的な流れはこれでよいのだが、実際には NODE_WHILEEXEC_TAG()からNODE_BREAKJUMP_TAG()までの 間に別のEXEC_TAGが入ることもありうる。例えば例外処理のrescueが途中に 入っているかもしれない。

while cond       # NODE_WHILEでEXEC_TAG()
  begin          # ここでrescueのためにまたEXEC_TAG()
    break 1
  rescue
  end
end

だからNODE_BREAKJUMP_TAG()したときのstruct tagNODE_WHILEで 積んだタグかどうかはわからない。その場合はPOP_TAG()の中で次のように retvalを伝播しているので、特に何も考えなくても次のタグへと返り値を渡 せるようになっている。

POP_TAG()

 818  #define POP_TAG()                       \
 819      if (_tag.prev)                      \
 820          _tag.prev->retval = _tag.retval;\
 821      prot_tag = _tag.prev;               \
 822  } while (0)

(eval.c)

図にすれば図6のようになるだろう。

(usetag)
図6: 返り値の伝播

例外

タグジャンプの使いかたの二つめの例として例外の取り扱いを見ていく。

raise

whileではsetjmp()側を先に見たので今回は趣向を変えて longjmp()側から見ていくことにしよう。raiseの実体rb_exc_raise()だ。

rb_exc_raise()

3645  void
3646  rb_exc_raise(mesg)
3647      VALUE mesg;
3648  {
3649      rb_longjmp(TAG_RAISE, mesg);
3650  }

(eval.c)

mesgは例外オブジェクト(Exception以下のクラスのインスタンス)である。 今回はTAG_RAISEでジャンプするらしいというところに注目しておこう。 そしてrb_longjmp()を思いきり簡単にしたものを以下に示す。

rb_longjmp()(簡約版)

static void
rb_longjmp(tag, mesg)
    int tag;
    VALUE mesg;
{
    if (NIL_P(mesg))
        mesg = ruby_errinfo;
    set_backtrace(mesg, get_backtrace(mesg));
    ruby_errinfo = mesg;
    JUMP_TAG(tag);
}

まあ当然と言えば当然だが、普通にJUMP_TAG()でジャンプするだけだ。

ruby_errinfoは何だろう。少しgrepしてみるとこの変数がRubyのグローバ ル変数$!の実体であるとわかった。この変数は現在発生中の例外を示してい るから、その実体のruby_errinfoも当然同じ意味を持つはずである。

全体像

▼ソースプログラム

begin
  raise('exception raised')
rescue
  'rescue clause'
ensure
  'ensure clause'
end

▼対応する構文木(nodedump-short

NODE_BEGIN
nd_body:
    NODE_ENSURE
    nd_head:
        NODE_RESCUE
        nd_head:
            NODE_FCALL
            nd_mid = 3857 (raise)
            nd_args:
                NODE_ARRAY [
                0:
                    NODE_STR
                    nd_lit = "exception raised":String
                ]
        nd_resq:
            NODE_RESBODY
            nd_args = (null)
            nd_body:
                NODE_STR
                nd_lit = "rescue clause":String
            nd_head = (null)
        nd_else = (null)
    nd_ensr:
        NODE_STR
        nd_lit = "ensure clause":String

パーサレベルでrescueensureの順番が決められているように、構文木で もまた順番が厳密に決まっている。必ずNODE_ENSUREが一番「上」、その次 にNODE_RESCUE、最後に本体(raiseのあるところ)が来る。 NODE_BEGINは何もしないノードなので、実際にはNODE_ENSUREが一番上に あると思っていい。

つまりプロテクトしたい本体よりもNODE_ENSURENODE_RESCUEが上に来るから、 単にEXEC_TAG()するだけでraiseを止められる。と言うよりも、そうできるよ うに構文木上で上にしている、というほうが正しいか。

ensure

ensureのノードNODE_ENSUREのハンドラを見ていく。

rb_eval()NODE_ENSURE

2634  case NODE_ENSURE:
2635    PUSH_TAG(PROT_NONE);
2636    if ((state = EXEC_TAG()) == 0) {
2637        result = rb_eval(self, node->nd_head);   (A-1)
2638    }
2639    POP_TAG();
2640    if (node->nd_ensr) {
2641        VALUE retval = prot_tag->retval;   (B-1)
2642        VALUE errinfo = ruby_errinfo;
2643
2644        rb_eval(self, node->nd_ensr);            (A-2)
2645        return_value(retval);              (B-2)
2646        ruby_errinfo = errinfo;
2647    }
2648    if (state) JUMP_TAG(state);            (B-3)
2649    break;

(eval.c)

このifを使った分岐はタグを扱う二つめのイディオムだ。 EXEC_TAG()してジャンプを止めておき、ensure節(node->nd_ensr)を 評価する、となっている。処理の流れについては問題ないだろう。

また評価値に関して考えてみる。仕様から確認すると、

begin
  expr0
ensure
  expr1
end

という文の場合、begin全体の値はensureのあるなしには関係なくexpr0の値に なる。それが反映されているのが(A-1,2)のところで、ensure節の評価値が 無碍に捨てられている。

また(B-1,3)では本体でジャンプが発生した場合の評価値を扱っている。 その場合の値はprot_tag->retvalに入っているのだったから、ensure節の 実行中にうっかり書き換えられないようローカル変数に退避する(B-1)。 そしてensure節の評価が終わったらreturn_value()で元に戻す(B-2)。 もし本体でジャンプが発生していなくても、つまりstate==0でも、 そのときはそもそもprot_tag->retvalは使われないからどうでもいい。

rescue

少し間が空いてしまったのでもう一度rescueの構文木を見ておこう。

▼ソースプログラム

begin
  raise()
rescue ArgumentError, TypeError
  'error raised'
end

▼対応する構文木(nodedump-short

NODE_BEGIN
nd_body:
    NODE_RESCUE
    nd_head:
        NODE_FCALL
        nd_mid = 3857 (raise)
        nd_args = (null)
    nd_resq:
        NODE_RESBODY
        nd_args:
            NODE_ARRAY [
            0:
                NODE_CONST
                nd_vid  = 4733 (ArgumentError)
            1:
                NODE_CONST
                nd_vid  = 4725 (TypeError)
            ]
        nd_body:
            NODE_STR
            nd_lit = "error raised":String
        nd_head = (null)
    nd_else = (null)

rescueの対象にしたい文(の構文木)がNODE_RESCUEの「下」にあることを 確認してほしい。

rb_eval()NODE_RESCUE

2590  case NODE_RESCUE:
2591  retry_entry:
2592    {
2593        volatile VALUE e_info = ruby_errinfo;
2594
2595        PUSH_TAG(PROT_NONE);
2596        if ((state = EXEC_TAG()) == 0) {
2597            result = rb_eval(self, node->nd_head); /* 本体を評価 */
2598        }
2599        POP_TAG();
2600        if (state == TAG_RAISE) { /* 本体で例外が発生した */
2601            NODE * volatile resq = node->nd_resq;
2602
2603            while (resq) { /* rescue節を順番に扱う */
2604                ruby_current_node = resq;
2605                if (handle_rescue(self, resq)) { /* この節で扱うなら */
2606                    state = 0;
2607                    PUSH_TAG(PROT_NONE);
2608                    if ((state = EXEC_TAG()) == 0) {
2609                        result = rb_eval(self, resq->nd_body);
2610                    }                            /* rescue節を評価 */
2611                    POP_TAG();
2612                    if (state == TAG_RETRY) { /* retryが発生したので */
2613                        state = 0;
2614                        ruby_errinfo = Qnil;  /* 例外は停止する */
2615                        goto retry_entry;     /* gotoに変換 */
2616                    }
2617                    if (state != TAG_RAISE) {  /* returnなどでも、*/
2618                        ruby_errinfo = e_info; /* 例外は停止する  */
2619                    }
2620                    break;
2621                }
2622                resq = resq->nd_head; /* 次のrescue節に進む */
2623            }
2624        }
2625        else if (node->nd_else) { /* else節があり、 */
2626            if (!state) { /* 例外が起きなかったときだけ評価する */
2627                result = rb_eval(self, node->nd_else);
2628            }
2629        }
2630        if (state) JUMP_TAG(state); /* 待っていないジャンプ */
2631    }
2632    break;

(eval.c)

多少コードサイズはあるが、ノードを地道に地道に扱うだけだから 難しくはない。handle_rescue()というのが初出だが、この関数は ちょっとわけあって今見るわけにいかない。効果だけ説明しておく。 プロトタイプは

static int handle_rescue(VALUE self, NODE *resq)

で、現在発生中の例外(ruby_errinfo)が resqで表されるクラス(例えばTypeError)の下位クラスであるか どうか判定する。selfを渡しているのは関数の中でresqを評価する ためにrb_eval()を呼ぶ必要があるからだ。


御意見・御感想・誤殖の指摘などは 青木峰郎 <aamine@loveruby.net> までお願いします。

『Rubyソースコード完全解説』 はインプレスダイレクトで御予約・御購入いただけます (書籍紹介ページへ飛びます)。

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