代入いろいろ
さて代入の話に入りますね。Rubyには、代入(あるいは代入っぽく見えるもの)がたくさんあります。
コード | 説明 | NODE |
---|---|---|
a = 100 | メソッドローカル変数への代入 | NODE_LASGN |
a = 100 | ブロックローカル変数への代入 | NODE_DASGN,NODE_DASGN_CURR |
$a = 100 | グローバル変数への代入 | NODE_GASGN |
@a = 100 | インスタンス変数への代入 | NODE_IASGN |
@@a = 100 | クラス変数への代入 | NODE_CVDECL,NODE_CVASGN |
A = 100 | 定数宣言 | NODE_CDECL |
a.b = 100 | 属性のセット | NODE_ATTRASGN |
a[2] = 100 | 属性のセット | NODE_ATTRASGN |
どれもコンパイルの基本はいっしょで、まず右辺を評価、次にその値を適切な方法で「代入」します。
case NODE_*ASGN: { COMPILE(ret, "lvalue", node->nd_value); if (!poped) { ADD_INSN(ret, nd_line(node), dup); } ... スタックトップの値を「代入」する命令列を生成 ... break; }
NODE_LASGN
メソッドローカル変数の個数は構文解析の段階で数え終わってるみたいです(RHG 12章)。おかげで変数それぞれにnd_cntという番号がついているので、ローカル変数への代入は、番号xxのローカル変数に値をセットせよ、ていう意味のsetlocal命令1個になります。
int idx = iseq->local_iseq->local_size + 2 - node->nd_cnt; ADD_INSN1(ret, nd_line(node), setlocal, INT2FIX(idx));
NODE_DASGN, NODE_DASGN_CURR
YARVアーキテクチャ に書かれているとおり、ブロックはそれじたいスコープを持ちます。つまり、ブロックの中ではじめて代入された変数は、ブロック内でだけ使えます。
こういう変数(ブロックローカル変数)への代入は setdynamic 命令に変換されます。
# エラーチェックは除いて抜粋 idx = get_dyna_var_idx(iseq, node->nd_vid, &lv, &ls); ADD_INSN2(ret, nd_line(node), setdynamic, INT2FIX(ls - idx), INT2FIX(lv));
ブロックがネストしている可能性もあるので、setdynamicは、「ネストの何個外側のブロックの、何番目のブロックローカル変数か」というパラメタを取ります。get_dyna_var_idx 関数は頑張ってネストをたどってこの情報を計算してくれてます(省略)。
NODE_GASGN
グローバル変数は setglobal に。わかりやすいですね。
ADD_INSN1(ret, nd_line(node), setglobal, (((long)node->nd_entry) | 1));
nd_entryには、グローバル変数用のシンボルテーブルのを変数名で引いたエントリが、構文解析器によって入れられてます。
NODE_IASGN
self のインスタンス変数へ代入する setinstancevariable 命令。
ADD_INSN1(ret, nd_line(node), setinstancevariable, ID2SYM(node->nd_vid));
引数として、変数名のシンボルを渡しています。インスタンス変数は、ローカル変数と違ってコンパイル時に個数が決まらないので、番号付けとかはできないんだと思います。なのでシンボル。
NODE_CVDECL, NODE_CVASGN
クラス変数は setclassvariable。同じく変数名のシンボルを。
ADD_INSN2(ret, nd_line(node), setclassvariable, ID2SYM(node->nd_vid), nd_type(node) == NODE_CVDECL ? Qtrue : Qfalse);
第2引数の true/false は気にしなくていいそうです。
NODE_CDECL
定数は setconstant と、変数名のシンボルです。
if (node->nd_vid) { ADD_INSN(ret, nd_line(node), putnil); ADD_INSN1(ret, nd_line(node), setconstant, ID2SYM(node->nd_vid)); } else { compile_cpath(ret, iseq, node->nd_else); ADD_INSN1(ret, nd_line(node), setconstant, ID2SYM(node->nd_else->nd_mid)); }
これも YARV Maniacs 第5回 参照ですが、どのクラス/モジュールに定数を定義するのかを、スタック経由で渡すようにコンパイルしています。ただの C なら nil を、::C や M::C のような形の定数アクセスなら、compile_cpath 関数が頑張って適切なオブジェクトをスタックに置くようコンパイルしてます(省略)。
NODE_ATTRASGN (1)
Ruby では属性への代入というのは特別な扱いになってなくて、
a.foo = 123
は、じつはオブジェクト a の foo= メソッドを引数 123 で呼び出しているだけなのです!
...と、むかし話に聞いて、すげーかっこいー!と思ったものでした。
class Foo def foo= (v) 456 end end a = Foo.new p (a.foo = 123) # 123 p (a.send(:foo=, 123)) # 456
どーん。でも実は、100%メソッド呼び出しそのものではなかったのでした。属性参照式全体の値は、右辺の値にしないといけないようです。foo= メソッドの返値は捨てるようにコンパイルします。
レシーバ.メソッド = 右辺
は、↓こうなります。右にコメントでスタックの様子を示しました。見事返値が右辺の値になってることがわかります。
putnil # [nil] レシーバ # [nil, obj] 右辺 # [nil, obj, rhs] setn 2 # [rhs, obj, rhs] send 1, :メソッド= # [rhs, ret] pop # [rhs]
setn は、スタックトップの値をスタックの深いところに上書きする命令です。send の 1 は、メソッドに渡す引数の個数です。
NODE_ATTRASGN (2)
a[10,20] = 30
は、じつはオブジェクト a の = メソッドを引数 10,20,30 で呼び出しているだけなのです!ということで、= も同じく NODE_ATTRASGN として処理されます。メソッドの引数が増えるので、スタックに積む量が増えて、setn で書き換える位置も深くなります。
putnil # [nil] レシーバ # [nil, obj] 配列添字1 # [nil, obj, i1] 配列添字2 # [nil, obj, i1, i2] ... # [nil, obj, i1, i2, ..] 右辺 # [nil, obj, i1, i2, .., rhs] setn N+1 # [rhs, obj, i1, i2, .., rhs] send N, :[]= # [rhs, ret] pop # [rhs]
こんな感じ。
実際にコンパイルするコードを見てみます。属性代入の場合も配列代入の場合もコードは共通です。引数の数の違いは補助関数 setup_arg が吸収します。
DECL_ANCHOR(args); argc = setup_arg(iseq, args, node, &flag);
詳しくは今度ふつうのメソッド呼び出しのコードを読むときに調べることにして、ここはスキップ。スタックに引数を全部積む命令列を、argsに出力してくれると思っておきます。
DECL_ANCHOR(recv); if (node->nd_recv == (NODE *) 1) { ADD_INSN(recv, nd_line(node), putself); } else { COMPILE(recv, "recv", node->nd_recv); }
続いてレシーバ式のコンパイル。何もないときはselfを積んでいます。
if (!poped) { ADD_INSN(ret, nd_line(node), putnil); ADD_SEQ(ret, recv); ADD_SEQ(ret, args); ADD_INSN1(ret, nd_line(node), setn, INT2FIX(FIX2INT(argc) + 1)); } else { ADD_SEQ(ret, recv); ADD_SEQ(ret, args); }
レシーバと引数をくっつけて、右辺値をsetnでコピー。
ADD_SEND_R(ret, nd_line(node), ID2SYM(node->nd_mid), argc, 0, INT2FIX(flag)); ADD_INSN(ret, nd_line(node), pop); break; }
send, pop, 完了です。