ひとり勉強会

ひとり楽しく勉強会

代入いろいろ

さて代入の話に入りますね。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, 完了です。