ひとり勉強会

ひとり楽しく勉強会

YARVソースコード勉強会(1-9)

まず最初にお知らせです。初回から今回までの内容を、スライドにまとめました。

なんだか長くて自分でも読み返すのがしんどかったので。。。
手っ取り早くて10分で読めるYARVソース読みの手がかりが欲しい!という方向けです。とは言っても、まだ一番最初のステップしか読めていないので、最適化やVMの実装についてはしばらくお待ち下さいゴメンナサイ、です。
あと、いつもそうですが、YARVはもちろんRubyも初心者の私が勝手気ままに調べて書いてる情報なので、あんまり信頼のおけるもんじゃないと思います。参考にされる場合は、そのつもりでお願いします。。。

YARVソースコード勉強会(9)

さてさて、今日は構文木からバイトコード命令列への変換、最終回です。今回はクラスやメソッドのなどの「定義」を扱うRuby構文のコンパイルを見ていきます。ほとんどがYARVの対応する数命令に変換される形なので、コンパイルはわりと簡単そうです。ではどうぞ。

NODE_DEFN 等

まず、クラスやメソッドを定義する文のコンパイルです。

ノード コード
NODE_DEFN def mname(...)
NODE_DEFS def obj.mname(...)
NODE_MODULE module MName
NODE_CLASS class CName
NODE_SCLASS class ≪obj

例えばJavaC++のような静的な言語では、この手の文はコンパイル時に解析されるので、ここまでで扱ってきたような実行時の文とは全く違う処理が必要となります。しかしRubyの場合は、クラス定義やメソッド定義も、実行時に左上から右下に実行される、単なる実行文です。なので、むしろ他の命令よりコンパイル自体は簡単かもしれません。

NODE_DEFN

def method()
  本体
end

は、

putnil
definemethod  :method, "本体"のコンパイル結果オブジェクト, 0

コンパイルされます。definemethod は、実行時に「現在のスコープになっているクラスに:methodメソッドを追加する」という動作をするYARV命令。最後の 0 は、「普通の」メソッド定義なことを表しています。

case NODE_DEFN:{
  VALUE iseqval = NEW_ISEQVAL(node->nd_defn,
                    rb_str_new2(rb_id2name(node->nd_mid)),
                    ISEQ_TYPE_METHOD);

  debugp_param("defn/iseq", iseqval);

  ADD_INSN (ret, nd_line(node), putnil);
  ADD_INSN3(ret, nd_line(node), definemethod,
    ID2SYM(node->nd_mid), iseqval, INT2FIX(0));
  if (!poped) {
      ADD_INSN(ret, nd_line(node), putnil);
  }
  debugp_param("defn", iseqval);
  break;
}

nd_defnが本体の構文木で、nd_midがメソッド名の入ったノードです。コンパイラのコードも、そのまんまですね。

NODE_DEFS

def expr.method()
  本体
end

NODE_DEFS は、特異メソッドの定義に対応するノードです。これは

exprを評価
definemethod :method, "本体", 1

こうなります。最後の 1 は特異メソッドの定義であることを意味します。スタックの一番上に置かれたオブジェクトに、特異メソッドが追加されます。

NODE_CLASS

class Klass < Super
  本体
end

は、大ざっぱにいうと、次のように defineclass 命令になります。

Klassの置かれる定数パスを評価
Superを評価
defineclass :Klass, "本体", 0

"本体"はdef文によるメソッド定義が並んでいることが多いですが、そこは"本体"のコンパイル中でdefinemethod命令の列になるので、クラス定義ではそれを単純に参照するだけで済みます。

case NODE_CLASS:{
  VALUE iseqval =
    NEW_CHILD_ISEQVAL(
      node->nd_body,
      make_name_with_str("<class:%s>",
        rb_id2name(node->nd_cpath->nd_mid)),
      ISEQ_TYPE_CLASS);
  compile_cpath(ret, iseq, node->nd_cpath);
  COMPILE(ret, "super", node->nd_super);
  ADD_INSN3(ret, nd_line(node), defineclass,
            ID2SYM(node->nd_cpath->nd_mid), iseqval,
            INT2FIX(0));

  if (poped) {
    ADD_INSN(ret, nd_line(node), pop);
  }
  break;
}

NODE_CDECL の説明のときも適当に飛ばしちゃいましたが、どこのスコープに定数(この場合はクラス名)を定義するのかは結構面倒な問題だそうで、その辺を頑張る命令列を生成するのが、compile_cpath関数です。あとはそのまんまな感じです。

NODE_SCLASS

class << expr
  本体
end

NODE_SCLASSは、特異クラスの定義です。これは最後の引数を 1 にした defineclass 命令です。

exprを評価
putnil
defineclass :singletonclass, "本体", 1

NODE_MODULE

module MBase::Module
  本体
end

NODE_MODULE はモジュール定義で、これも、似たようなものなので defineclass 命令になります。最後の引数 2 で区別されています。

MBaseを評価
putnil
defineclass :Module, "本体", 2

alias

Ruby の alias には2種類あります。メソッドに別名をつけるalias(NODE_ALIAS)と、グローバル変数に別名をつけるalias(NODE_VALIAS)です。

ノード コード
NODE_ALIAS alias newmethod oldname
NODE_ALIAS alias :newmethod :oldname
NODE_VALIAS alias $newname $oldname

YARVではどちらも、一個の alias 命令に変換されます。命令の第一引数で、どっちの別名かを区別します。

;; alias newmethod oldname
alias false, :newmethod, :oldname

;; alias $newname $oldname
alias true, :newname, :oldname

NODE_UNDEF

undef メソッド名

一度定義したメソッドを取り消す処理です。わたし、aliasとundefの存在は今日始めて知りました。(^_^; こんなのあったんですね。

case NODE_UNDEF:{
  if (nd_type(node->u2.node) != NODE_LIT) {
    rb_bug("undef args must be NODE_LIT");
  }
  ADD_INSN1(ret, nd_line(node), undef,
    ID2SYM(rb_to_id(node->u2.node->nd_lit)));
  if (!poped) {
    ADD_INSN(ret, nd_line(node), putnil);
  }
  break;
}

コンパイルは、これも単純にYARVのundef命令への変換です。

NODE_DEFINED

これが最後!

defined? e

変数などなどが定義されているかどうかを返す式です。定義されていれば式の種別を表す文字列、いなければ偽を返します。コンパイルはdefined_expr補助関数が全面的に担当しています。

case NODE_DEFINED:{
  if (!poped) {
    LABEL *lfinish = NEW_LABEL(nd_line(node));
    defined_expr(iseq, ret, node->nd_head, lfinish, Qtrue);
    ADD_LABEL(ret, lfinish);
  }
  break;
}

基本的に、Rubyのdefined?式は、YARVのdefined命令に変換されます。

defined  種類,  変数名など,  返値文字列が必要か?

例えば defined?(@iv) なら、こうです。

case NODE_IVAR:
  ADD_INSN(ret, nd_line(node), putnil);
  ADD_INSN3(ret, nd_line(node), defined, INT2FIX(DEFINED_IVAR),
            ID2SYM(node->nd_vid), needstr);
  return 1;

インスタンス変数の定義を調べたい!という意味のフラグ DEFINED_IVAR が先頭のパラメータです。このようなフラグが変数などなどの種類に応じて全て定義されているので、それを使います。第二引数は、変数名を表すシンボルです。最後のneedstrは、ユーザーがコードに書いたdefined?をコンパイルする際は常にtrueになっています。
他には例えば、ブロックが渡されているかどうか調べる defined?(yield) では、別にフラグが用意されています。

case NODE_YIELD:
  ADD_INSN(ret, nd_line(node), putnil);
  ADD_INSN3(ret, nd_line(node), defined,
            INT2FIX(DEFINED_YIELD), 0,
    needstr);
  return 1;

メソッドのdefined?を調べる場合には少し注意が必要です。

defined?(aaa.bbb)

という式を評価するには、aaaがどういうオブジェクトか調べて(これはaaaを評価しなければわかりません)そこにbbbメソッドがあるかどうか判定する必要があります。ところが、bbb以前にaaaが定義されていない可能性もあります。そういう場合も何事もなく偽を返すために、

  • まずはaaaのdefinedを検査
  • aaaを評価する
  • aaa.bbbがdefinedかどうか検査

という3段の命令列を生成します。

case NODE_CALL:
case NODE_VCALL:
case NODE_FCALL:
  if (nd_type(node) == NODE_CALL) {
    LABEL *lcont = NEW_LABEL(nd_line(node));

    defined_expr(iseq, ret, node->nd_recv, lfinish, Qfalse);
    ADD_INSNL(ret, nd_line(node), branchif, lcont) ;
    ADD_INSN(ret, nd_line(node), putnil);
    ADD_INSNL(ret, nd_line(node), jump, lfinish);

    ADD_LABEL(ret, lcont);
    COMPILE(ret, "defined/recv", node->nd_recv);
    ADD_INSN3(ret, nd_line(node), defined,
      INT2FIX(DEFINED_METHOD),
      ID2SYM(node->nd_mid), needstr);
  }
  ...

最初のレシーバのdefined検査で偽が帰ってきた場合、すぐにlfinishへジャンプするコードを生成しています(ここではdefined結果の文字列は不要なので、needstrにQfalseを指定しています。)。そうでなければ、改めてrecvを評価して、defined命令で検査、と。
他の要素にdefined?する場合も似たような雰囲気なので、省略。

まとめ

というわけで、定義関係のコンパイル部分を読み終わりました!

これにて [compile step 1] が無事?終了です。年末はお休みです。来年からは、コード生成部分の続き compile step 3 〜 5 に移ろうと思います。3,4,5 と3回で行ければいいな。

では、よいお年を!