2011年12月2日金曜日

Ruby の「条件式としての範囲式」の正体を探る

ご苦労様です、サイオス 那賀です。

Ruby 言語における「条件式としての範囲式」というやつが、どうも腑に落ちません (「Ruby 1.9.2 リファレンスマニュアル 演算子式 条件式としての範囲式」)。

> ["foo", "bar", "BEGIN", "hoge", "fuga", "END", "buzz"].select { |w|
    (w === "BEGIN" .. w === "END")? true: false
  }
=> ["BEGIN", "hoge", "fuga", "END"]
> 

これはオブジェクトのリテラルではなく、言語仕様としての、何かしらの特殊な「何か」ですよね? その証拠に上記でも、3 項演算子を削って、Ruby が括弧内を条件式と判別できないようにすると、オブジェクトにはなり得ません。

> ["foo", "bar", "BEGIN", "hoge", "fuga", "END", "buzz"].select { |w|
    (w === "BEGIN" .. w === "END")
  }
ArgumentError: bad value for range
> 

ということは、ここでループ中にその状態を抱えているのは Ruby のスタック上の「何か」ですよね? なまじ何でもオブジェクトな Ruby なだけに、これも一見 Range のオブジェクトに見えるんですが…。せめて、実装上の正体だけでも見ておこうと思います。

# Ruby でも、遅延評価とか名前渡しとかできるとカッコ良かったんですけどね

Yacc の入力ファイルがあると思います。

$ find . -name "*.y"
./ruby-1.8.7-p299/parse.y

ありました。該当するトークンを探します。

static int
yylex()
{
  ...
  retry:
    switch (c = nextc()) {
      ...
      case '.':
        lex_state = EXPR_BEG;
        if ((c = nextc()) == '.') {
            if ((c = nextc()) == '.') {
                return tDOT3;
            }
            pushback(c);
            return tDOT2;
        }
        ...

static struct {
    ID token;
    const char *name;
} op_tbl[] = {
    {tDOT2,     ".."},
    {tDOT3,     "..."},
    {'+',       "+"},
    ...

"tDOT2" と "tDOT3" が "..", "..." のトークンのようです。どうパースしていますかね?

arg : lhs '=' arg
    ...
    | arg tDOT2 arg
        {
            value_expr($1);
            value_expr($3);
            $$ = NEW_DOT2($1, $3);
            if (nd_type($1) == NODE_LIT && FIXNUM_P($1->nd_lit) &&
                nd_type($3) == NODE_LIT && FIXNUM_P($3->nd_lit)) {
                deferred_nodes = list_append(deferred_nodes, $$);
            }
        }
    ...

NEW_DOT2(), NEW_DOT3() とやらで、パースツリーのノードを作っています。node.h:

#define NEW_DOT2(b,e) NEW_NODE(NODE_DOT2,b,e,0)
#define NEW_DOT3(b,e) NEW_NODE(NODE_DOT3,b,e,0)

ノードの型としては "NODE_DOT2" や "NODE_DOT3" で上がって行くようです。これは Range でも条件範囲式でも同じようですので、上で何らかの置換処理をしているんでしょう。if 式での処理を見てみます。

primary : literal
        | kIF expr_value then
          compstmt
          if_tail
          kEND
            {
                $$ = NEW_IF(cond($2), $4, $5);
                fixpos($$, $2);
                if (cond_negative(&$$->nd_cond)) {
                    NODE *tmp = $$->nd_body;
                    $$->nd_body = $$->nd_else;
                    $$->nd_else = tmp;
                }
            }

式が条件式だった時には、cond() 関数 → cond0() 関数で何かの変換をしているようです。

static NODE*
cond0(node)
    NODE *node;
{
  ...
  switch (nd_type(node)) {
    ...
    case NODE_DOT2:
    case NODE_DOT3:
      node->nd_beg = range_op(node->nd_beg);
      node->nd_end = range_op(node->nd_end);
      if (nd_type(node) == NODE_DOT2) nd_set_type(node,NODE_FLIP2);
      else if (nd_type(node) == NODE_DOT3) nd_set_type(node, NODE_FLIP3);
      node->nd_cnt = local_append(internal_id());
      if (!e_option_supplied()) {
        int b = literal_node(node->nd_beg);
        int e = literal_node(node->nd_end);
        if ((b == 1 && e == 1) || (b + e >= 2 && RTEST(ruby_verbose))) {
          parser_warn(node, "range literal in condition");
        }
      }
      break;
      ...

ありました、条件式に指定された場合には、パースツリーのノード型を変えています。どうやら Ruby の「条件式としての範囲式」は、内部的には、ドットの数によってそれぞれ「flip2」「flip3」と呼ばれているようです。

となれば、処理がされる箇所を探すのは簡単です。eval.c:

static VALUE
rb_eval(self, n)
    VALUE self;
    NODE *n;
{
  ...
  switch (nd_type(node)) {
    ...
    case NODE_FLIP2:          /* like AWK */
      {
        VALUE *flip = rb_svar(node->nd_cnt);
        if (!flip) rb_bug("unexpected local variable");
        if (!RTEST(*flip)) {
          if (RTEST(rb_eval(self, node->nd_beg))) {
            *flip = RTEST(rb_eval(self, node->nd_end))?Qfalse:Qtrue;
            result = Qtrue;
          }
          else {
            result = Qfalse;
          }
        }
        else {
          if (RTEST(rb_eval(self, node->nd_end))) {
            *flip = Qfalse;
          }
          result = Qtrue;
        }
      }
      break;

なるほど、フリップフロップですか。ここでの鍵は rb_svar() でしょうか。

VALUE *
rb_svar(cnt)
    int cnt;
{
    struct RVarmap *vars = ruby_dyna_vars;
    ID id;

    if (!ruby_scope->local_tbl) return NULL;
    if (cnt >= ruby_scope->local_tbl[0]) return NULL;
    id = ruby_scope->local_tbl[cnt+1];
    while (vars) {
        if (vars->id == id) return &vars->val;
        vars = vars->next;
    }
    if (ruby_scope->local_vars == 0) return NULL;
    return &ruby_scope->local_vars[cnt];
}

Ruby のスコープのローカルで、対応する変数の入れモノを返してくれるようです。PUSH_SCOPE() を見ると、ruby_scope はスタックを push するたびに作られているようですので、スレッドごとに独立するんでしょう。

#define PUSH_SCOPE() do {               \
    volatile int _vmode = scope_vmode;  \
    struct SCOPE * volatile _old;       \
    NEWOBJ(_scope, struct SCOPE);       \
    OBJSETUP(_scope, 0, T_SCOPE);       \
    _scope->local_tbl = 0;              \
    _scope->local_vars = 0;             \
    _scope->flags = 0;                  \
    _old = ruby_scope;                  \
    ruby_scope = _scope;                \
    scope_vmode = SCOPE_PUBLIC

というわけで、「条件式としての範囲式」は、内部的には Range 型オブジェクトなどではなく、「flip2」「flip3」と呼ばれる言語仕様であり、状態はローカルのスコープごとに持っているようです。

注意: このアプローチには時に、言語設計者がせっかく隠蔽してくれているものを、実装レベルで見て分かったような気になってしまう罠が隠れています。例えば、Scala の吐いた class ファイルの逆コンパイルをしたところで、実装の理解には役立っても、言語の理解には全く役立たないどころか、むしろ有害だったりします。

では。

0 件のコメント:

コメントを投稿