NODE_SUPER
最初は super のコンパイルです。親クラスの同じメソッドに処理を回すやつです。super には二種類あって、
super(100) # 引数つきsuper super # 引数なしsuper
引数つきの方は NODE_SUPER、引数なしの方は NODE_ZSUPER と構文解析の段階で分かれています。ZSUPER 版は、現在のメソッドに渡された引数をそのまま親メソッドに引き渡すという意味のRubyの構文です。
YARVアーキテクチャ の頃はYARVのマシン命令レベルでも、super と zsuper に分かれていたようですが、現在のバージョンでは invokesuper という同じ命令にまとまっています。
superとメソッド呼び出しとの違いは、おおまかには、invokesuper命令を使うかsuper命令を使うか、だけです。なので流れは先週と一緒ですね。コンパイル後の命令列は、以下の順番です。
- レシーバをスタックに積む
- 引数をスタックに積む
- ブロックが付属してればその処理
- 呼び出し (invokesuper)
case NODE_SUPER: case NODE_ZSUPER:{ ... 略 ... /* dummy reciever */ if (nd_type(node) == NODE_ZSUPER) { ADD_INSN1(ret, nd_line(node), putobject, Qfalse); } else { ADD_INSN1(ret, nd_line(node), putobject, Qtrue); }
あ、でも、superの場合レシーバはselfに決まってるので、レシーバは重要ではありません。現在の実装では、superとzsuperを区別するフラグを受け渡す用に使っていました。
DECL_ANCHOR(args); if (nd_type(node) == NODE_SUPER) { argc = setup_arg(iseq, args, node, &flag); } else { /* NODE_ZSUPER */ ... 省略 ... for (i = 0; i < liseq->argc; i++) { int idx = liseq->local_size - i; ADD_INSN1(args, nd_line(node), getlocal, INT2FIX(idx)); } } ADD_SEQ(ret, args);
続いて引数を積む命令列です。引数ありの場合は、メソッドの時と同じ補助関数setup_argが使えます。引数なしの場合は、現在のメソッドに渡された引数をコピーしてスタックに積みます。引数はローカル変数テーブルの端の方に詰まっているのでそれをgetlocalでスタックに積んでるようです。
ローカル変数は書き換わるかもしれないですが、その場合は書き換わった値が渡る...
class A def f(x); p x; end end class B<A def f(x); x="me"; super; end end B.new.f "you" # "me" を表示
...んですね。ちょっぴり意外でした。
/* block */ if (parent_block) { if (parent_block & 1) { flag |= VM_CALL_ARGS_BLOCKARG_BIT; ADD_SEQ(ret, (LINK_ANCHOR *)(parent_block & (~1))); } else { block = parent_block; } }
続いてブロックの準備。これはメソッド呼び出しの時と全く同じですね。
前回の宿題として、「この parent_block & 1 ってなんだろう?」が残ってました。じつは未だによくわからないんですけど、これは ブロックと Proc オブジェクトの分離 関係、かな。
Block と Proc を分離しない場合は、ブロックは常に「Procオブジェクトをスタックに積む」コードにコンパイルされてて、parent_block に最下位ビットONで入っていた。けど、それをやらなくなったのでこのコードは現時点では使われていない、ような気がする、みたいな。(あやふやです。)
この辺りはもう使われていないコードなので気にしなくて良いそうです。
ADD_INSN3(ret, nd_line(node), invokesuper, argc, block, INT2FIX(flag));
ともかく、最後はinvokesuper命令で終了です。
NODE_YIELD
yield は、呼び出すものがブロックに変わるだけで、本質的にはメソッド呼び出しです。ただし、ブロックにブロックを渡したりはしません。あとレシーバを考える必要もないです。その分 call や super より簡単です。
- 引数をスタックに積んで
- 呼び出し(invokeblock)
呼び出し命令は invokeblock です。
case NODE_YIELD:{ DECL_ANCHOR(args); // // おきまりの、args に引数積み積み命令を格納する処理 // (省略) // ADD_SEQ(ret, args); ADD_INSN2(ret, nd_line(node), invokeblock, INT2FIX(argc), INT2FIX(flag));
NODE_RETURN
return は
- メソッド定義の直下からreturnする場合
- メソッドの中のブロックの中からreturnする場合
でコンパイル結果の命令列が変わってきます。それ以外の場所(クラス定義の直下や、トップレベル)でreturnしようとするとエラーです。
どちらも、breakのコンパイル と、考え方はまったくおんなじ。前者は、周りのensure節をその場に展開してから、ジャンプ命令で脱出します。
// 抜粋 case NODE_RETURN:{ ADD_INSN(ret, nd_line(node), emptstack); COMPILE(ret, "return nd_stts (return val)", node->nd_stts); add_ensure_iseq(ret, iseq); ADD_INSN(ret, nd_line(node), leave);
emptstack はその名の通り、現在のメソッドで使ったスタックを空にクリアする命令です。
ブロックの中から一気にreturnするには、breakと同様、例外を使います。
// 抜粋 COMPILE(ret, "return nd_stts (return val)", node->nd_stts); ADD_INSN1(ret, nd_line(node), throw, INT2FIX(0x01) /* TAG_RETURN */ );
変数/定数読み取り
Rubyには変数にも色々種類があります...というのは、代入文のところでやりました。
種類 | NODE | 命令 |
---|---|---|
メソッドローカル変数 | NODE_LVAR | getlocal |
ブロックローカル変数 | NODE_DVAR | getdynamic |
グローバル変数 | NODE_GVAR | getglobal |
インスタンス変数 | NODE_IVAR | getinstancevariable |
クラス変数 | NODE_CVAR | getclassvariable |
変数の値を読む式は、変数の種類に応じて、対応するYARVの1命令にコンパイルされます。例えばローカル変数ならこんな感じ。
case NODE_LVAR:{ if (!poped) { int idx = iseq->local_iseq->local_size + 2 - node->nd_cnt; debugs("idx: %d\n", idx); ADD_INSN1(ret, nd_line(node), getlocal, INT2FIX(idx)); } break; }
種類 | NODE | 命令 |
---|---|---|
定数 | NODE_CONST | getconstant |
obj::定数 | NODE_COLON2 | getconstant |
::定数 | NODE_COLON3 | getconstant |
- ありの場合は少しややこしいですが、定数も基本は
getconstant 1命令へコンパイルされます定数を取ってくる対象のクラスオブジェクトをスタックに積んで、getconstant する、という命令列にコンパイルされます(nilが積んであればカレントスコープ、falseが積んであればトップレベルスコープから検索。)。。ただし、「定数インラインキャッシュ」の最適化が有効になっていると、こんな感じにキャッシュチェック命令が入ります。
lstart: getinlinecache lend getconstant :定数名 setinlinecache lend:
Rubyの定数はあとから幾らでも書き換えられるので、あまり「定数」っぽくはありません。それでも「実際には一度代入したらあとは変更しない」という使われ方が多いようです。ということで、前回値を読んだときから定数書き換えがおこっていなければ、定数の検索をせずにキャッシュから直接値を返す、という流れ。そのための特別な命令が、getinlinecache と setinlinecache です。詳しい動作はVMの方のコードを読むときにでも。参考:るびま
種類 | NODE | 命令 |
---|---|---|
$1,$2,... | NODE_NTH_REF | getspecial |
$&,$+,$',$` | NODE_BACK_REF | getspecial |
組み込み変数(プログラミング言語 Ruby リファレンスマニュアル)のうち、ほとんどはgetglobal命令で値のとれる、グローバル変数です。ただし、正規表現マッチで使われる$1,$2,...だけは他のグローバル変数とは区別されて、getspecialというYARV命令に落ちるようです。
種類 | NODE | 命令 |
---|---|---|
rescue C=>v | NODE_ERRINFO | - |
変数の項でまとめる話じゃないかもしれませんが、resuce節で変数と例外のクラスを宣言した場合、NODE_ERRINFOというノードがくっついてきます。詳細略ですが、getglobal $! して、クラスチェックをした後変数vにsetdynamicするコードにコンパイルされています。ぜんぜん違いました!rescue C=>v; は、getdynamic $! -> setlocal v という流れになります。
NODE_ARRAY
配列式のコンパイルです。たとえば [a,b,c] という式は
eval a eval b eval c newarray 3
こういう命令列になります。newarray がスタックから値を拾ってきて配列オブジェクトを作る命令です。コンパイル処理は compile_array という補助関数に丸投げです。
case NODE_ARRAY:{ compile_array(iseq, ret, node, Qtrue); if (poped) { ADD_INSN(ret, nd_line(node), pop); } break; } case NODE_ZARRAY:{ if (!poped) { ADD_INSN1(ret, nd_line(node), newarray, INT2FIX(0)); } break; }
ついでに ZARRAY は [] の時で、newarray 0 にコンパイルされています。
あと、るびま で解説されているように、中身が全部リテラルの配列では、わざわざスタックの積みおろしをしなくても作るべき配列オブジェクトがわかります。この最適化処理も compile_array の中に入っています。
NODE_HASH
ハッシュリテラルです。
{"aaa",3,"bbb",4} {"ccc"=>5,"ddd"=>6}
newarrayのハッシュ版命令「newhash」があるので、実はこれも、左から順に要素をスタックに積んで、newhashするだけなのです。重要なとこだけコンパイラのコードを抜粋すると、こうです。
case NODE_HASH:{ ... 略 ... compile_array(iseq, list, node->nd_head, Qfalse); size = OPERAND_AT(POP_ELEMENT(list), 0); ADD_SEQ(ret, list); ADD_INSN1(ret, nd_line(node), newhash, size);
compile_array して(最後のQfalseはリテラル最適化をしないという意味です)、最後の命令、つまり newarray を取り除いて、かわりに newhash を付け加えています。
奇数個しか要素のないハッシュリテラル {1,2,3} などは、構文解析の段階ではじかれていて、ここでは気にしていません。
NODE_DOT系
"a".."z" 0...10
のようなRangeオブジェクトを表す式です。ドット2個の NODE_DOT2 とドット3個の NODE_DOT3 があって、どちらもnewrange命令にコンパイルされて引数のフラグで区別されます。
case NODE_DOT2: case NODE_DOT3:{ int flag = type == NODE_DOT2 ? INT2FIX(0) : INT2FIX(1); COMPILE(ret, "min", (NODE *) node->nd_beg); COMPILE(ret, "max", (NODE *) node->nd_end); if (poped) { ADD_INSN(ret, nd_line(node), pop); ADD_INSN(ret, nd_line(node), pop); } else { ADD_INSN1(ret, nd_line(node), newrange, flag); } break; }
NODE_FLIP系
NODE_DOTの範囲式が条件式として使われると、特別な意味になります。そういう位置にある範囲式はNODE_DOTではなくNODE_FLIPとしてコンパイルされます。
式1 .. 式2
は「式1が真になるまでは偽を返し、その後は式2が真を返すまでは真を返します。式2が真になれば状態は偽に戻ります」となっているそうです。つまり、この範囲式を評価するには、今「式1が真になるまで」の状態なのか、「その後は式2が真を返すまでは」の状態なのかをどこかに記憶しておく必要があります。
現在のYARVの実装では、範囲式の状態を保存する領域へのアクセスは、getspecial で行います。
ADD_INSN2(ret, nd_line(node), getspecial, INT2FIX(node->nd_cnt),
INT2FIX(0));
構文解析の段階で node->nd_cnt に$1,$2などとは重ならない番号が入っているので、それを利用しています。状態を記憶するために、式1待ちのときはfalse、式2待ちのときはtrueを入れます。というわけで、生成されるコードはこちらの通り。
getspecial branchif lend # 状態がtrueならlend:へジャンプ eval 式1 dup branchunless lfin # 式1 が falseなら式全体がfalse # 式1 が trueなら、それを状態に記憶 setspecial lend: eval 式2 branchunless ltrue # 式2がfalseなら式全体がtrue # 式2がtrueなら、falseを状態に記憶 putobject false setspecial ltrue: putobject true lfin:
NODE_MATCH 系
ノードは3種類ありますが、要するにどれも =~ です。基本は、単純に =~ メソッドを呼ぶだけです。
case NODE_MATCH: case NODE_MATCH2: case NODE_MATCH3:{ ... 思いっきり略 ... COMPILE(recv, "reciever", node->nd_recv); COMPILE(val, "value", node->nd_value); ... 思いっきり略 ... ADD_SEQ(ret, recv); ADD_SEQ(ret, val); ADD_SEND(ret, nd_line(node), ID2SYM(idEqTilde), INT2FIX(1));
ただし、るびま で解説されている最適化が入ることがあります。idEqTilde を send する代わりに、「=~が再定義されていなければ、正規表現マッチ処理をする。再定義されていれば諦めてsendする」という意味の opt_regexpmatch 命令を使うという最適化です。Ruby では正規表現のマッチは非常によく使われるので、メソッド呼び出しのコストを回避する価値があるというわけです。
こちらがその最適化版を生成するコード
if (iseq->compile_data->option->specialized_instruction) { /* TODO: detect by node */ if (recv->last == recv->anchor.next && INSN_OF(recv->last) == BIN(putobject) && nd_type(node) == NODE_MATCH2) { ADD_SEQ(ret, val); ADD_INSN1(ret, nd_line(node), opt_regexpmatch1, OPERAND_AT(recv->last, 0)); } else { ADD_SEQ(ret, recv); ADD_SEQ(ret, val); ADD_INSN(ret, nd_line(node), opt_regexpmatch2); } }
使われてる正規表現がリテラルなら、opt_regexpmatch1。そうでなければ opt_regexpmatch1 と、さらに細かく分かれています。
BEGIN と END
大文字の BEGIN
BEGIN { ... }
は、そのファイルの他の部分より先に必ず評価されるブロックで、構文解析の段階で、NODE_PRELUDE というノードになります。構文解析器がソースコードの中のBEGINブロックを全部取り出して
node nd_head : ソース中のBEGINブロックの中身 nd_body : BEGIN以外の部分の中身
というノードを作ってくれるので、その順番にコンパイルするだけです。
case NODE_PRELUDE:{ COMPILE_POPED(ret, "prelude", node->nd_head); COMPILE_(ret, "body", node->nd_body, poped); break; }
大文字の END
END { ... }
は、この文が実行されたときに、終了処理としてブロックを登録します。これは単純にpostexeというYARVの1命令になります。
case NODE_POSTEXE:{
VALUE block = NEW_CHILD_ISEQVAL(node, make_name_for_block(iseq), ISEQ_TYPE_BLOCK);
ADD_INSN1(ret, nd_line(node), postexe, block);
NODE_OPTBLOCK
YARVの最適化の一つとして、「ブロックのインライン化」があります。例えば
3.times { ブロックの中身 }
をブロックを作ったりyieldで呼んだりすると、スタック操作が無駄に発生してしまって効率がよくありません。あと、ブロックを使うとredoやnextが例外になってしまうのも避けたいところです。というわけで、こういう簡単なループがブロックで書かれているときは、while文に変換されます。
e = 0 while e < 3 do lredo: ブロックの中身 lnext: e = e + 1 end
「実はされません。とても微妙なバグがあるため、この機能はオフになっています。」だそうです。むむむー残念。
この最適化変換でwhileの中に移動されたブロックを表すノードが、OPTBLOCKです。
case NODE_OPTBLOCK:{ /* for optimize */ LABEL *redo_label = NEW_LABEL(0); LABEL *next_label = NEW_LABEL(0); iseq->compile_data->start_label = next_label; iseq->compile_data->redo_label = redo_label; ADD_LABEL(ret, redo_label); COMPILE_(ret, "optblock body", node->nd_head, 1 /* pop */ ); ADD_LABEL(ret, next_label); ADD_INSN(ret, 0, opt_checkenv); break; }
動的文字列&正規表現
文字列リテラルにも色々ありまして
NODE | 例 |
---|---|
NODE_DSTR | "a = #{a}" |
NODE_EVSTR | "#{a}" |
文字列がputstring命令になるのは、式展開が含まれていない、単純な文字列リテラルのときだけです。
"値は #{a} と #{b} です"
のように式展開が含まれる場合は
NODE_DSTR NODE_STR "値は " NODE_EVSTR a NODE_STR " と " NODE_EVSTR b NODE_STR "です"
というノードが構文解析で作られて
putobject "値は " [aを評価する命令列] tostring putobject " と " [bを評価する命令列] tostring putobject " です" concatstrings 5
みたいに、左から順に文字列をスタックに積んでいって、concatstrings命令でひとつにまとめる、というコードにコンパイルされます。
NODE | 例 |
---|---|
NODE_XSTR | `echo` |
NODE_DXSTR | `ec#{104.chr}o` |
NODE_DSYM | :$gvar |
NODE_DREGX | /#{gets}/ |
NODE_DREGX_ONCE | /#{gets}/o |
式展開が入りうるリテラルは他にも、シンボルと正規表現があります。それぞれ、DSTRと同じ方法で一旦文字列を作ってから、.internメソッドを呼ぶか、YARVのtoregexp命令に変換されます。"once"正規表現は、onceを実現するために定数のキャッシュと似た onceinlinecache という仕組みを使うようコンパイルされてました。