YARVソースコード勉強会(1-9)
NODE_DEFN 等
まず、クラスやメソッドを定義する文のコンパイルです。
ノード | コード |
---|---|
NODE_DEFN | def mname(...) |
NODE_DEFS | def obj.mname(...) |
NODE_MODULE | module MName |
NODE_CLASS | class CName |
NODE_SCLASS | class ≪obj |
例えばJavaやC++のような静的な言語では、この手の文はコンパイル時に解析されるので、ここまでで扱ってきたような実行時の文とは全く違う処理が必要となります。しかし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; }
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; }
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回で行ければいいな。
では、よいお年を!