ひとり勉強会

ひとり楽しく勉強会

NODE_CALL 等

続いて、メソッド呼び出しです。3種類のノードがありますが、それぞれの内訳はこの通り。

case NODE_CALL:
case NODE_FCALL:
case NODE_VCALL:{ /* VCALL: variable or call */
  /*
    call:  obj.method(...)
    fcall: func(...)
    vcall: func
   */

vcallは、機能としては単純に引数0個のfcallだと思います。たぶん、引数も括弧もないからローカル変数かもしんないね、っていうエラーメッセージを出すため「だけ」にフラグのためなどに分かれているのかな。

メソッド呼び出しは、4ステップに分かれます。

  • receiver (objかself) をスタックに積む
  • 引数をスタックに積む
  • ブロックが付属してればその処理
  • メソッド呼び出し(send)

順番にコードを追って行きましょう。

#if SUPPORT_JOKE
  ...
#endif

ここはスルー力が試されています。

/* reciever */
if (type == NODE_CALL) {
  COMPILE(recv, "recv", node->nd_recv);
}
else if (type == NODE_FCALL || type == NODE_VCALL) {
  ADD_INSN(recv, nd_line(node), putself);
}

まずはreceiver。そのまんまです。

/* args */
if (nd_type(node) != NODE_VCALL) {
  argc = setup_arg(iseq, args, node, &flag);
}
else {
   argc = INT2FIX(0);
}

続いて引数。代入の話のときに後回しにした補助関数、setup_arg です。その名の通りメソッド呼び出しの引数をセットアップする命令列を作ってくれそうな関数です。かるーく見てみましょう。

static VALUE
setup_arg(yarv_iseq_t *iseq, LINK_ANCHOR *args, NODE *node, VALUE *flag)
{
  ... 略 ...

  if (argn && nd_type(argn) == NODE_BLOCK_PASS) {
    COMPILE(arg_block, "block", argn->nd_body);
    *flag |= VM_CALL_ARGS_BLOCKARG_BIT;
    argn = argn->nd_head;
  }

ブロック引数があった場合…つまり、引数の最後に "&" つきで手続きオブジェクトを渡したばあいです。arg_blockは、普通の引数をスタックの後ろに積んだその後に積まれます。普通の引数を評価する命令列はこちら

setup_argn:
  if (argn) {
    switch (nd_type(argn)) {
      case NODE_SPLAT: { ... 略 ...
      case NODE_ARGSCAT: { ... 略 ...
      case NODE_ARGSPUSH: { ... 略 ...
      default: { ... 略 ...

カンマ区切りで並んでたり *arg で展開してたりするパターンで場合分けされてて大変なのですが、要は左から右にひたすら引数を積んでいます。疲れてきたので詳細略です。*arg で呼ばれた場合を表す VM_CALL_ARGS_SPLAT_BIT というフラグも用意されていました。

  ADD_SEQ(ret, recv);
  ADD_SEQ(ret, args);

で、setup_arg の話は終わりにして NODE_CALL に戻ります。とりあえずレシーバと引数を評価する命令列まで作り終えました。つぎは、ブロックです。

  /* block */
  if (parent_blockval) {
    if (parent_blockval & 1) {
      flag |= VM_CALL_ARGS_BLOCKARG_BIT;
      ADD_SEQ(ret, (LINK_ANCHOR *)(parent_blockval & (~1)));
    }
    else {
      block = parent_blockval;
    }
  }

obj.call { ブロック } のブロックの中身は NODE_ITERコンパイル済みです。NODE_CALL のこの箇所には、そのコンパイル済み命令列が parent_blockval に入った状態で到達します。
えーとそれで、最下位ビットを見て分岐しているようなんですけど、これなんでしたっけ。ビットが立っている場合は、「手続きオブジェクトをスタックに積む」命令が parent_blockval に入っていることを前提としたコードになっています。そうでない場合は、ブロックの中身をコンパイルした命令列なはずで、それを一旦ローカルのblock変数に覚えています。

  switch (nd_type(node)) {
    case NODE_VCALL:
      flag |= VM_CALL_VCALL_BIT;
      /* VCALL is funcall, so fall through */
    case NODE_FCALL:
      flag |= VM_CALL_FCALL_BIT;
  }

fcallなのかvcallなのかのフラグを立てて

  ADD_SEND_R(ret, nd_line(node), ID2SYM(mid), argc,
	     block, INT2FIX(flag));

send命令を生成。send命令は、メソッド名のシンボル(mid)と、呼び出し側が渡した引数の個数(mid)、もしあればブロック(block)、そして様々なフラグ(flag)の4引数を取っています。

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

あとはおきまりのpopチェック。完了です。