Rubyの yield / Proc#call はどこのブロックを呼ぶのか

この記事で参照するコード/実行例はRuby trunk (r61423 / Ruby 2.5.0-rc1以降 2.5.0未満)のものを使っている。

# 背景

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Aが呼ぶBがyieldした時にそのブロックで呼ばれるyield時に使われるblock_handlerはBのcfp-&gt;ep[-1]になる気がするけど何故Aに渡されたブロックが使われるのか全然わからなかったが、まあ動いてるっぽいので帰ってから考えることにした</p>&mdash; asm volatile(&quot;int3&quot;); (@k0kubun) <a href="https://twitter.com/k0kubun/status/915771566251679744?ref_src=twsrc%5Etfw">2017年10月5日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

```rb 
def a
  b do
    yield
  end
end

def b(&block)
  block.call
end

a { 1 } #=> 1
```

これ(`yield`が)なんで動くのっていう話。`yield`がマニュアル通り「メソッドに渡されたブロック」を評価するものと解釈したら自明では？と思うかもしれませんが、中途半端にVMのコードを読んだ理解だと納得できなかったのです。
(余談だけど、こういうケースでちょっと不安なので`a(&block)`と定義して`b do ... end`の中で`block.call`したいんだけど、コードの可読性のためにした変更なのにrubocopに`yield`にしろと怒られたのはちょっとイラッと来た)

まあ面倒なので2か月くらい放置していたのですが、今日VMをいじっていたら以下のコードが動かなくなりました。

```rb
def timeout(sec)
  bl = proc do
    y = Thread.start {
      sleep sec
    }
    result = yield(sec)
    y.kill
    y.join
    return result
  end
  bl.call
  caller(0)
end

timeout(100) do
  foo.bar
end
```

先ほどの`yield`の挙動の理解でいくと、`bl.call`時に一旦`Proc#call`のためのCの関数に入り、Cの世界からRubyの`proc do ... end`のISeqを実行するためにVMが起動され、そのVMの中で`yield`に到達して`invokeblock`命令により新しいcontrol frameが積まれ同じVMの中で`foo.bar`のブロックが処理される…という挙動が期待できます。(というか、何もいじらなければそうなるはず) この場合、`return result`でメソッドを抜けるので、`caller(0)`には到達しません。

ところが、実際には`bl.call`でVMを起動する時のISeqが`foo.bar`のあるブロックになってしまい、そこで`foo.bar`を呼び出した後普通に次の行に抜け、本来大域脱出により到達しないはずの`caller(0)`の呼び出しに到達してしまいました。これが本来内部的にどういう挙動になっているかを理解していないので、どこが間違ってるのかわからず直せない、という状態です。

この記事はこれらの内部的な挙動を理解するためのメモです。

# 前提知識
## ブロックにブロックを渡すことはできるが、yieldはそれを参照しない
冒頭の疑問に関しては、(1)自分のRubyの挙動の理解があやふやである、(2)内部でどのように実現されているかわからない、といった問題があったが、(1)に関してはRubyのスクリプトをいくつか適当に実行してみれば理解できる。この見出しがそれによって導き出された結論である。

```rb
def a
  b do |&block|
    block.call
  end
end

def b(&block)
  block.call { 1 }
end

a #=> 1
```

なんとこれは動作するのである。bが呼び出すブロックは冒頭の例ではaに渡されたブロックを実行しており、なおかつこの例ではaにブロックが渡っていないにも関わらず、だ。まあでも見掛け上これは動作してほしい気はする。

一方、

```rb
def a
  b do |&block|
    yield
  end
end

def b(&block)
  block.call { 1 }
end

a
# Traceback (most recent call last):
#         3: from a.rb:11:in `<main>'
#         2: from a.rb:2:in `a'
#         1: from a.rb:8:in `b'
# a.rb:3:in `block in a': no block given (yield) (LocalJumpError)
```

これは動かないのだ。つまり、冒頭の例とも比較して考えると、`yield`というのはそもそも現在のcontrol frameに渡されているブロックを呼び出すのではなく、そのブロックを内包しているメソッドに渡されたブロックを呼び出すようになっている。

## yieldではブロックにブロックを渡せない
`block.call`ではブロックに対しブロックを渡せるのは見た。では`yield`ではできるのか？

```rb
def a
  b do |&block|
    block.call
  end
end

def b
  yield { 1 }
end

a
# a.rb:8: syntax error, unexpected '{'
#   yield { 1 }
#         ^
# a.rb:11: syntax error, unexpected end-of-input, expecting keyword_end
```

コンパイルも通らない。

従って、ブロックA(`#a`内のブロック)がAに渡されたブロックB(`#b`内のブロック)を呼び出すケースというのは、Aの呼び出しがブロックBつき`block.call`になっており、かつAも`block.call`でブロックBを呼び出す場合しか存在しない。

「ブロックにブロックを渡すことはできるが、yieldはそれを参照しない」という前提から、ブロックAの呼び出しの際に直接渡されるブロックBのProcオブジェクトは必ず引数(ブロックパラメータ)から参照されることになり、わざわざブロックAのcontrol frameにつっこんでおく必要はそもそもないのである。

じゃあ、ブロックAのcontrol frameのblock_handlerのところには`#a`に渡されたブロックを突っ込んでおく余地がありそうだが、実際どのように渡されているかを見ていく。

# yieldされるISeqはどのように`invokeblock`に渡されるか
## 一番簡単なケース
```rb 
def a
  yield
end

a { 1 } #=> 1
```

まあ一番簡単なのはこれですね。

```
== disasm: #<ISeq:<main>@a.rb:0>========================================
== catch table
| catch type: break  st: 0010 ed: 0015 sp: 0000 cont: 0015
== disasm: #<ISeq:block in <main>@a.rb:5>===============================
== catch table
| catch type: redo   st: 0001 ed: 0002 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0002 sp: 0000 cont: 0002
|------------------------------------------------------------------------
0000 nop                                                              (   5)[Bc]
0001 putobject_OP_INT2FIX_O_1_C_ [Li]
0002 leave            [Br]
|------------------------------------------------------------------------
0000 putspecialobject 1                                               (   1)[Li]
0002 putobject        :a
0004 putiseq          a
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 pop
0010 putself                                                          (   5)[Li]
0011 send             <callinfo!mid:a, argc:0, FCALL>, <callcache>, block in <main>
0015 leave
== disasm: #<ISeq:a@a.rb:1>=============================================
0000 invokeblock      <callinfo!argc:0, ARGS_SIMPLE>                  (   2)[LiCa]
0002 leave            [Re]
```

`invokeblock`で実行されることを一応確認。

### `invokeblock` は `block_handler` をどのように見つけるか

```c 
/**
  @c method/iterator
  @e yield(args)
  @j yield を実行する。
 */
DEFINE_INSN
invokeblock
(CALL_INFO ci)
(...)
(VALUE val)  // inc += 1 - ci->orig_argc;
{
    struct rb_calling_info calling;
    calling.argc = ci->orig_argc;
    calling.block_handler = VM_BLOCK_HANDLER_NONE;
    calling.recv = GET_SELF();

    val = vm_invoke_block(ec, GET_CFP(), &calling, ci);
    if (val == Qundef) {
        RESTORE_REGS();
        NEXT_INSN();
    }
}
```

```c 
static VALUE
vm_invoke_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci)
{
    VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
    VALUE type = GET_ISEQ()->body->local_iseq->body->type;
    int is_lambda = FALSE;

    if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) ||
        block_handler == VM_BLOCK_HANDLER_NONE) {
        rb_vm_localjump_error("no block given (yield)", Qnil, 0);
    }

  again:
    switch (vm_block_handler_type(block_handler)) {
      case block_handler_type_iseq:
        {
            const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler);
            return vm_invoke_iseq_block(ec, reg_cfp, calling, ci, is_lambda, captured);
        }
      case block_handler_type_ifunc:
        {
            const struct rb_captured_block *captured = VM_BH_TO_IFUNC_BLOCK(block_handler);
            return vm_invoke_ifunc_block(ec, reg_cfp, calling, ci, captured);
        }
      case block_handler_type_proc:
        is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler));
        block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler));
        goto again;
      case block_handler_type_symbol:
        return vm_invoke_symbol_block(ec, reg_cfp, calling, ci, VM_BH_TO_SYMBOL(block_handler));
    }
    VM_UNREACHABLE(vm_invoke_block: unreachable);
    return Qnil;
}

/* cfp から block_handler を取り出す */
PUREFUNC(static inline VALUE VM_CF_BLOCK_HANDLER(const rb_control_frame_t * const cfp));
static inline VALUE
VM_CF_BLOCK_HANDLER(const rb_control_frame_t * const cfp)
{
    const VALUE *ep = VM_CF_LEP(cfp);
    return VM_ENV_BLOCK_HANDLER(ep);
}

/* cfp->ep から、一番直近の VM_ENV_FLAG_LOCAL (0x02) が立っているepを見つける */
PUREFUNC(static inline const VALUE *VM_CF_LEP(const rb_control_frame_t * const cfp));
static inline const VALUE *
VM_CF_LEP(const rb_control_frame_t * const cfp)
{
    return VM_EP_LEP(cfp->ep);
}

/* ep[0] に VM_ENV_FLAG_LOCAL (0x02) が立っているepが見付かるまで ep[-1] から前のepを遡る */
PUREFUNC(static inline const VALUE *VM_EP_LEP(const VALUE *));
static inline const VALUE *
VM_EP_LEP(const VALUE *ep)
{
    while (!VM_ENV_LOCAL_P(ep)) {
        ep = VM_ENV_PREV_EP(ep);
    }
    return ep;
}

/* ep[0] に VM_ENV_FLAG_LOCAL (0x02) が立っていたら1を返す */
static inline int
VM_ENV_LOCAL_P(const VALUE *ep)
{
    return VM_ENV_FLAGS(ep, VM_ENV_FLAG_LOCAL) ? 1 : 0;
}

static inline unsigned long
VM_ENV_FLAGS(const VALUE *ep, long flag)
{
    VALUE flags = ep[VM_ENV_DATA_INDEX_FLAGS];
    VM_ASSERT(FIXNUM_P(flags));
    return flags & flag;
}

#define VM_ENV_DATA_INDEX_ME_CREF    (-2) /* ep[-2] */
#define VM_ENV_DATA_INDEX_SPECVAL    (-1) /* ep[-1] */
#define VM_ENV_DATA_INDEX_FLAGS      ( 0) /* ep[ 0] */
#define VM_ENV_DATA_INDEX_ENV        ( 1) /* ep[ 1] */
#define VM_ENV_DATA_INDEX_ENV_PROC   ( 2) /* ep[ 2] */

enum {
    /* env flag */
    VM_ENV_FLAG_LOCAL       = 0x0002,
    VM_ENV_FLAG_ESCAPED     = 0x0004,
    VM_ENV_FLAG_WB_REQUIRED = 0x0008
};

/* ep[-1] に格納されているポインタから 0x03 のビットを取り除いて返す */
static inline const VALUE *
VM_ENV_PREV_EP(const VALUE *ep)
{
    VM_ASSERT(VM_ENV_LOCAL_P(ep) == 0);
    return GC_GUARDED_PTR_REF(ep[VM_ENV_DATA_INDEX_SPECVAL]);
}

#define GC_GUARDED_PTR_REF(p) VM_TAGGED_PTR_REF((p), 0x03)
#define VM_TAGGED_PTR_REF(v, mask) ((void *)((v) & ~mask))

/* ep[-1] を参照して返す */
static inline VALUE
VM_ENV_BLOCK_HANDLER(const VALUE *ep)
{
    VM_ASSERT(VM_ENV_LOCAL_P(ep));
    return ep[VM_ENV_DATA_INDEX_SPECVAL];
}
```

冒頭のツイートをした時は`VM_CF_LEP`のことを覚えていたがこの記事を書き始めた時は忘れていた…

さて`VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);`に行についてだが、各関数にコメントをつけた通り、以下のような手順で`block_handler`を取り出す。

* `cfp->ep`を`ep`とし、`ep[0]`に`VM_ENV_FLAG_LOCAL`(0x02) が立っているepを見つけるまで、`ep = ep[-1]`を繰り返し、lepとする
* `lep[-1]`を`block_handler`とする

このコードを見る限りだと、`ep[-1]`(`ep[VM_ENV_DATA_INDEX_SPECVAL]`)には以下の値が入っていそうに見える (予想)

* `ep[0]`に`VM_ENV_FLAG_LOCAL`が立っている場合: 
  * `block_handler`が入っている
* `ep[0]`に`VM_ENV_FLAG_LOCAL`が立っていない場合: 
  * 一つ前のepが入っている

### どのようなフレームに`VM_ENV_FLAG_LOCAL`が立つか
[前に書いた記事](https://qiita.com/k0kubun/items/6c35aa1a9c2e03c46b07)にある程度書いてあった。

- rb_iseq_eval_main
  - `VM_FRAME_MAGIC_EVAL | VM_FRAME_FLAG_FINISH`
- rb_iseq_eval
  - `VM_FRAME_MAGIC_TOP | VM_ENV_FLAG_LOCAL | VM_FRAME_FLAG_FINISH`
- vm_call_iseq_setup_normal
  - `VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL`
- vm_call_cfunc_with_frame
  - `VM_FRAME_MAGIC_CFUNC | VM_FRAME_FLAG_CFRAME | VM_ENV_FLAG_LOCAL`

ブロックのフレームに立つのかどうかが気になるが、とりあえずこの節の理解には不要なのでスキップ。Rubyのメソッドには`vm_call_iseq_setup_normal`が使われるのでこのフレームのepはlepということになる。

なので、`vm_call_iseq_setup_normal`が呼ばれる時にブロックハンドラーっぽい奴がそのフレームの`ep[-1]`に積まれれば動きそう。

### Rubyのメソッド呼び出しではどのように`block_handler`が作られるか

上述したISeqに書かれているsend命令の定義を見ていく

```c
/**
  @c method/iterator
  @e invoke method.
  @j メソッド呼び出しを行う。ci に必要な情報が格納されている。
 */
DEFINE_INSN
send
(CALL_INFO ci, CALL_CACHE cc, ISEQ blockiseq)
(...)
(VALUE val) // inc += - (int)(ci->orig_argc + ((ci->flag & VM_CALL_ARGS_BLOCKARG) ? 1 : 0));
{
    struct rb_calling_info calling;

    vm_caller_setup_arg_block(ec, reg_cfp, &calling, ci, blockiseq, FALSE);
    vm_search_method(ci, cc, calling.recv = TOPN(calling.argc = ci->orig_argc));
    CALL_METHOD(&calling, ci, cc);
}
```

```c 
#define CALL_METHOD(calling, ci, cc) do { \
    VALUE v = (*(cc)->call)(ec, GET_CFP(), (calling), (ci), (cc)); \
    if (v == Qundef) { \
        RESTORE_REGS(); \
        NEXT_INSN(); \
    } \
    else { \
        val = v; \
    } \
} while (0)

/* cc->call は最初 vm_call_general になっている */

static VALUE
vm_call_general(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    return vm_call_method(ec, reg_cfp, calling, ci, cc);
}

static inline VALUE
vm_call_method(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    VM_ASSERT(callable_method_entry_p(cc->me));

    if (cc->me != NULL) {
        switch (METHOD_ENTRY_VISI(cc->me)) {
          case METHOD_VISI_PUBLIC: /* likely */
            return vm_call_method_each_type(ec, cfp, calling, ci, cc);

          case METHOD_VISI_PRIVATE:
            if (!(ci->flag & VM_CALL_FCALL)) {
                enum method_missing_reason stat = MISSING_PRIVATE;
                if (ci->flag & VM_CALL_VCALL) stat |= MISSING_VCALL;

                cc->aux.method_missing_reason = stat;
                CI_SET_FASTPATH(cc, vm_call_method_missing, 1);
                return vm_call_method_missing(ec, cfp, calling, ci, cc);
            }
            return vm_call_method_each_type(ec, cfp, calling, ci, cc);

          case METHOD_VISI_PROTECTED:
            if (!(ci->flag & VM_CALL_OPT_SEND)) {
                if (!rb_obj_is_kind_of(cfp->self, cc->me->defined_class)) {
                    cc->aux.method_missing_reason = MISSING_PROTECTED;
                    return vm_call_method_missing(ec, cfp, calling, ci, cc);
                }
                else {
                    /* caching method info to dummy cc */
                    struct rb_call_cache cc_entry;
                    cc_entry = *cc;
                    cc = &cc_entry;

                    VM_ASSERT(cc->me != NULL);
                    return vm_call_method_each_type(ec, cfp, calling, ci, cc);
                }
            }
            return vm_call_method_each_type(ec, cfp, calling, ci, cc);

          default:
            rb_bug("unreachable");
        }
    }
    else {
        return vm_call_method_nome(ec, cfp, calling, ci, cc);
    }
}

static VALUE
vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    switch (cc->me->def->type) {
      case VM_METHOD_TYPE_ISEQ:
        CI_SET_FASTPATH(cc, vm_call_iseq_setup, TRUE);
        return vm_call_iseq_setup(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_NOTIMPLEMENTED:
      case VM_METHOD_TYPE_CFUNC:
        CI_SET_FASTPATH(cc, vm_call_cfunc, TRUE);
        return vm_call_cfunc(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_ATTRSET:
        CALLER_SETUP_ARG(cfp, calling, ci);
        rb_check_arity(calling->argc, 1, 1);
        cc->aux.index = 0;
        CI_SET_FASTPATH(cc, vm_call_attrset, !((ci->flag & VM_CALL_ARGS_SPLAT) || (ci->flag & VM_CALL_KWARG)));
        return vm_call_attrset(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_IVAR:
        CALLER_SETUP_ARG(cfp, calling, ci);
        rb_check_arity(calling->argc, 0, 0);
        cc->aux.index = 0;
        CI_SET_FASTPATH(cc, vm_call_ivar, !(ci->flag & VM_CALL_ARGS_SPLAT));
        return vm_call_ivar(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_MISSING:
        cc->aux.method_missing_reason = 0;
        CI_SET_FASTPATH(cc, vm_call_method_missing, TRUE);
        return vm_call_method_missing(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_BMETHOD:
        CI_SET_FASTPATH(cc, vm_call_bmethod, TRUE);
        return vm_call_bmethod(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_ALIAS:
        cc->me = aliased_callable_method_entry(cc->me);
        VM_ASSERT(cc->me != NULL);
        return vm_call_method_each_type(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_OPTIMIZED:
        switch (cc->me->def->body.optimize_type) {
          case OPTIMIZED_METHOD_TYPE_SEND:
            CI_SET_FASTPATH(cc, vm_call_opt_send, TRUE);
            return vm_call_opt_send(ec, cfp, calling, ci, cc);
          case OPTIMIZED_METHOD_TYPE_CALL:
            CI_SET_FASTPATH(cc, vm_call_opt_call, TRUE);
            return vm_call_opt_call(ec, cfp, calling, ci, cc);
          default:
            rb_bug("vm_call_method: unsupported optimized method type (%d)",
                   cc->me->def->body.optimize_type);
        }

      case VM_METHOD_TYPE_UNDEF:
        break;

      case VM_METHOD_TYPE_ZSUPER:
        return vm_call_zsuper(ec, cfp, calling, ci, cc, RCLASS_ORIGIN(cc->me->owner));

      case VM_METHOD_TYPE_REFINED: {
        const rb_cref_t *cref = rb_vm_get_cref(cfp->ep);
        VALUE refinements = cref ? CREF_REFINEMENTS(cref) : Qnil;
        VALUE refinement;
        const rb_callable_method_entry_t *ref_me;

        refinement = find_refinement(refinements, cc->me->owner);

        if (NIL_P(refinement)) {
            goto no_refinement_dispatch;
        }
        ref_me = rb_callable_method_entry(refinement, ci->mid);

        if (ref_me) {
            if (cc->call == vm_call_super_method) {
                const rb_control_frame_t *top_cfp = current_method_entry(ec, cfp);
                const rb_callable_method_entry_t *top_me = rb_vm_frame_method_entry(top_cfp);
                if (top_me && rb_method_definition_eq(ref_me->def, top_me->def)) {
                    goto no_refinement_dispatch;
                }
            }
            cc->me = ref_me;
            if (ref_me->def->type != VM_METHOD_TYPE_REFINED) {
                return vm_call_method(ec, cfp, calling, ci, cc);
            }
        }
        else {
            cc->me = NULL;
            return vm_call_method_nome(ec, cfp, calling, ci, cc);
        }

      no_refinement_dispatch:
        if (cc->me->def->body.refined.orig_me) {
            cc->me = refined_method_callable_without_refinement(cc->me);
        }
        else {
            VALUE klass = RCLASS_SUPER(cc->me->defined_class);
            cc->me = klass ? rb_callable_method_entry(klass, ci->mid) : NULL;
        }
        return vm_call_method(ec, cfp, calling, ci, cc);
      }
    }

    rb_bug("vm_call_method: unsupported method type (%d)", cc->me->def->type);
}

static VALUE
vm_call_iseq_setup(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    const rb_iseq_t *iseq = def_iseq_ptr(cc->me->def);
    const int param_size = iseq->body->param.size;
    const int local_size = iseq->body->local_table_size;
    const int opt_pc = vm_callee_setup_arg(ec, calling, ci, cc, def_iseq_ptr(cc->me->def), cfp->sp - calling->argc, param_size, local_size);
    return vm_call_iseq_setup_2(ec, cfp, calling, ci, cc, opt_pc, param_size, local_size);
}

static inline VALUE
vm_call_iseq_setup_2(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc,
                     int opt_pc, int param_size, int local_size)
{
    if (LIKELY(!(ci->flag & VM_CALL_TAILCALL))) {
        return vm_call_iseq_setup_normal(ec, cfp, calling, ci, cc, opt_pc, param_size, local_size);
    }
    else {
        return vm_call_iseq_setup_tailcall(ec, cfp, calling, ci, cc, opt_pc);
    }
}

static inline VALUE
vm_call_iseq_setup_normal(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc,
                          int opt_pc, int param_size, int local_size)
{
    const rb_callable_method_entry_t *me = cc->me;
    const rb_iseq_t *iseq = def_iseq_ptr(me->def);
    VALUE *argv = cfp->sp - calling->argc;
    VALUE *sp = argv + param_size;
    cfp->sp = argv - 1 /* recv */;

    vm_push_frame(ec, iseq, VM_FRAME_MAGIC_METHOD | VM_ENV_FLAG_LOCAL, calling->recv,
                  calling->block_handler, (VALUE)me,
                  iseq->body->iseq_encoded + opt_pc, sp,
                  local_size - param_size,
                  iseq->body->stack_max);
    return Qundef;
}

static inline rb_control_frame_t *
vm_push_frame(rb_execution_context_t *ec,
              const rb_iseq_t *iseq,
              VALUE type,
              VALUE self,
              VALUE specval,
              VALUE cref_or_me,
              const VALUE *pc,
              VALUE *sp,
              int local_size,
              int stack_max)
{
    rb_control_frame_t *const cfp = ec->cfp - 1;
    int i;

    vm_check_frame(type, specval, cref_or_me, iseq);
    VM_ASSERT(local_size >= 0);

    /* check stack overflow */
    CHECK_VM_STACK_OVERFLOW0(cfp, sp, local_size + stack_max);

    ec->cfp = cfp;

    /* setup new frame */
    cfp->pc = (VALUE *)pc;
    cfp->iseq = (rb_iseq_t *)iseq;
    cfp->self = self;
    cfp->block_code = NULL;

    /* setup vm value stack */

    /* initialize local variables */
    for (i=0; i < local_size; i++) {
        *sp++ = Qnil;
    }

    /* setup ep with managing data */
    VM_ASSERT(VM_ENV_DATA_INDEX_ME_CREF == -2);
    VM_ASSERT(VM_ENV_DATA_INDEX_SPECVAL == -1);
    VM_ASSERT(VM_ENV_DATA_INDEX_FLAGS   == -0);
    *sp++ = cref_or_me; /* ep[-2] / Qnil or T_IMEMO(cref) or T_IMEMO(ment) */
    *sp++ = specval     /* ep[-1] / block handler or prev env ptr */;
    *sp   = type;       /* ep[-0] / ENV_FLAGS */

    cfp->ep = sp;
    cfp->sp = sp + 1;

#if VM_DEBUG_BP_CHECK
    cfp->bp_check = sp + 1;
#endif

    if (VMDEBUG == 2) {
        SDR();
    }

    return cfp;
}
```

`cc->call`の初期値が`vm_call_general`になっていることは[以前の記事](https://qiita.com/k0kubun/items/d1c1a868913d76f61073)で解説したので割愛。

すると、`specval`という引数名の意味はよくわからないがこれが "block handler or prev env ptr" として `ep[-1]`にセットされるようだ。

で、このパス(sendによるRubyのメソッドの呼び出し)だと`calling->block_handler`が渡される。

### send命令で`calling->block_handler`はどのように作られるか
send命令のコードを見ると、名前的に`vm_caller_setup_arg_block`が怪しい。

```c 
static void
vm_caller_setup_arg_block(const rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
                          struct rb_calling_info *calling, const struct rb_call_info *ci, rb_iseq_t *blockiseq, const int is_super)
{
    if (ci->flag & VM_CALL_ARGS_BLOCKARG) {
        VALUE block_code = *(--reg_cfp->sp);

        if ((ci->flag & VM_CALL_ARGS_BLOCKARG_BLOCKPARAM) &&
            !VM_ENV_FLAGS(VM_CF_LEP(reg_cfp), VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM)) {
            calling->block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
        }
        else if (NIL_P(block_code)) {
            calling->block_handler = VM_BLOCK_HANDLER_NONE;
        }
        else if (SYMBOL_P(block_code) && rb_method_basic_definition_p(rb_cSymbol, idTo_proc)) {
            const rb_cref_t *cref = vm_env_cref(reg_cfp->ep);
            if (cref && !NIL_P(cref->refinements)) {
                VALUE ref = cref->refinements;
                VALUE func = rb_hash_lookup(ref, block_code);
                if (NIL_P(func)) {
                    /* TODO: limit cached funcs */
                    func = rb_func_proc_new(refine_sym_proc_call, block_code);
                    rb_hash_aset(ref, block_code, func);
                }
                block_code = func;
            }
            calling->block_handler = block_code;
        }
        else {
            calling->block_handler = vm_to_proc(block_code);
        }
    }
    else if (blockiseq != NULL) { /* likely */
        struct rb_captured_block *captured = VM_CFP_TO_CAPTURED_BLOCK(reg_cfp);
        captured->code.iseq = blockiseq;
        calling->block_handler = VM_BH_FROM_ISEQ_BLOCK(captured);
    }
    else {
        if (is_super) {
            calling->block_handler = GET_BLOCK_HANDLER();
        }
        else {
            calling->block_handler = VM_BLOCK_HANDLER_NONE;
        }
    }
}
```

ていうかそもそもよく見るとsend命令の第三オペランドは`ISEQ blockiseq`である。これが`rb_iseq_t *blockiseq`としてこの関数に渡ってくる。

なお、send命令の[`ci->flag`はISeqのdisasmから確認できるようになっている](https://github.com/ruby/ruby/blob/1942e9254744bc9629d2c7ea0e670e75051311de/iseq.c#L1517-L1534)。フラグは以下のものがセットされる

```c 
#define VM_CALL_ARGS_SPLAT      (0x01 << VM_CALL_ARGS_SPLAT_bit)
#define VM_CALL_ARGS_BLOCKARG   (0x01 << VM_CALL_ARGS_BLOCKARG_bit)
#define VM_CALL_ARGS_BLOCKARG_BLOCKPARAM (0x01 << VM_CALL_ARGS_BLOCKARG_BLOCKPARAM_bit)
#define VM_CALL_FCALL           (0x01 << VM_CALL_FCALL_bit)
#define VM_CALL_VCALL           (0x01 << VM_CALL_VCALL_bit)
#define VM_CALL_ARGS_SIMPLE     (0x01 << VM_CALL_ARGS_SIMPLE_bit)
#define VM_CALL_BLOCKISEQ       (0x01 << VM_CALL_BLOCKISEQ_bit)
#define VM_CALL_KWARG           (0x01 << VM_CALL_KWARG_bit)
#define VM_CALL_KW_SPLAT        (0x01 << VM_CALL_KW_SPLAT_bit)
#define VM_CALL_TAILCALL        (0x01 << VM_CALL_TAILCALL_bit)
#define VM_CALL_SUPER           (0x01 << VM_CALL_SUPER_bit)
#define VM_CALL_OPT_SEND        (0x01 << VM_CALL_OPT_SEND_bit)
```

そして上の方に書いた結果を再度引用すると、

```
0011 send             <callinfo!mid:a, argc:0, FCALL>, <callcache>, block in <main>
```

となっており、`VM_CALL_ARGS_BLOCKARG`がない。というかこれは`foo(&block)`みたいな奴向けだった気がする。今回は放置。

というわけなので、sendの第三オペランド`blockiseq`の中身がある限りは`else if (blockiseq != NULL) { /* likely */`で止まる。`block in <main>`とあるとおり、これはブロックがあれば渡りそうである。確かに"likely"っぽい。

もう一度該当部分を引用する。

```c 
    else if (blockiseq != NULL) { /* likely */
        struct rb_captured_block *captured = VM_CFP_TO_CAPTURED_BLOCK(reg_cfp);
        captured->code.iseq = blockiseq;
        calling->block_handler = VM_BH_FROM_ISEQ_BLOCK(captured);
    }
```

```c 
static struct rb_captured_block *
VM_CFP_TO_CAPTURED_BLOCK(const rb_control_frame_t *cfp)
{
    VM_ASSERT(!VM_CFP_IN_HEAP_P(GET_EC(), cfp));
    return (struct rb_captured_block *)&cfp->self;
}

static inline VALUE
VM_BH_FROM_ISEQ_BLOCK(const struct rb_captured_block *captured)
{
    VALUE block_handler = VM_TAGGED_PTR_SET(captured, 0x01);
    VM_ASSERT(VM_BH_ISEQ_BLOCK_P(block_handler));
    return block_handler;
}

#define VM_TAGGED_PTR_SET(p, tag)  ((VALUE)(p) | (tag))

struct rb_captured_block {
    VALUE self;
    const VALUE *ep;
    union {
        const rb_iseq_t *iseq;
        const struct vm_ifunc *ifunc;
        VALUE val;
    } code;
};
```

`(VALUE)&cfp->self | 0x01`が`block_handler`の実態のようです。そして`((struct rb_captured_block *)&cfp->self)->code.iseq`にsend命令の第三オペランドの`blockiseq`がセットされるようですね。

で、`cfp->self`には`calling->recv`がセットされる(かつ、send命令中で`calling.recv = TOPN(calling.argc = ci->orig_argc)`が入る)んだけど、それは`&cfp->self`とは多分関係ない。

ここで、cfpの中身の定義を見てみる。

```c 
typedef struct rb_control_frame_struct {
    const VALUE *pc;            /* cfp[0] */
    VALUE *sp;                  /* cfp[1] */
    const rb_iseq_t *iseq;      /* cfp[2] */
    VALUE self;                 /* cfp[3] / block[0] */
    const VALUE *ep;            /* cfp[4] / block[1] */
    const void *block_code;     /* cfp[5] / block[2] */ /* iseq or ifunc */

#if VM_DEBUG_BP_CHECK
    VALUE *bp_check;            /* cfp[6] */
#endif
} rb_control_frame_t;
```

`cfp[3]`~`cfp[5]`の構造が`struct rb_captured_block`に似ている。コメントに`block[0]`~`block[2]`というのが書いてあるが、これは多分`rb_captured_block`にキャストされることを意味しているのだと思う。

つまり、`&cfp->self`というのは、control frameの構造体のうち後半だけを持ってくることを意味しており、これを`struct rb_captured_block *`に変換して`code.iseq`にセットするのはどういうことかというと、呼び出し側のcfpの`cfp[5]`(`cfp->block_code`)にセットされることを意味する。

従って、`yield`するメソッドは、`yield`するメソッドを呼び出したフレームの後半部分を`block_handler`経由で参照しに来るということだ。

### rb_block_handler_type には何があるか
`#a`を呼び出す時にsend命令で`calling->block_handler`が`vm_push_frame`で`specval`として`ep[-1]`にセットされ、その`calling->block_handler`は呼び出し側のcfpの後半を差しており、後半の最後のフィールドである`cfp->block_code`にはsend命令第三オペランドの`blockiseq`がセットされることまではわかった。

普通のRubyのメソッドのepはlepであり、`VM_CF_BLOCK_HANDLER`は直近のlepから`ep->[-1]`を`block_handler`として取り出すので、`block_handler`が適切に渡りそうなこともわかった。

が、以下の`vm_invoke_block`のこの後の挙動を理解するには、typeを知る必要がある。

```c 
static VALUE
vm_invoke_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci)
{
    VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
    VALUE type = GET_ISEQ()->body->local_iseq->body->type;
    int is_lambda = FALSE;

    if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) ||
        block_handler == VM_BLOCK_HANDLER_NONE) {
        rb_vm_localjump_error("no block given (yield)", Qnil, 0);
    }

  again:
    switch (vm_block_handler_type(block_handler)) {
      case block_handler_type_iseq:
        {
            const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler);
            return vm_invoke_iseq_block(ec, reg_cfp, calling, ci, is_lambda, captured);
        }
      case block_handler_type_ifunc:
        {
            const struct rb_captured_block *captured = VM_BH_TO_IFUNC_BLOCK(block_handler);
            return vm_invoke_ifunc_block(ec, reg_cfp, calling, ci, captured);
        }
      case block_handler_type_proc:
        is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler));
        block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler));
        goto again;
      case block_handler_type_symbol:
        return vm_invoke_symbol_block(ec, reg_cfp, calling, ci, VM_BH_TO_SYMBOL(block_handler));
    }
    VM_UNREACHABLE(vm_invoke_block: unreachable);
    return Qnil;
}

enum rb_block_handler_type {
    block_handler_type_iseq,
    block_handler_type_ifunc,
    block_handler_type_symbol,
    block_handler_type_proc
};

static inline enum rb_block_handler_type
vm_block_handler_type(VALUE block_handler)
{
    if (VM_BH_ISEQ_BLOCK_P(block_handler)) {
        return block_handler_type_iseq;
    }
    else if (VM_BH_IFUNC_P(block_handler)) {
        return block_handler_type_ifunc;
    }
    else if (SYMBOL_P(block_handler)) {
        return block_handler_type_symbol;
    }
    else {
        VM_ASSERT(rb_obj_is_proc(block_handler));
        return block_handler_type_proc;
    }
}

static inline int
VM_BH_ISEQ_BLOCK_P(VALUE block_handler)
{
    if ((block_handler & 0x03) == 0x01) {
#if VM_CHECK_MODE > 0
        struct rb_captured_block *captured = VM_TAGGED_PTR_REF(block_handler, 0x03);
        VM_ASSERT(imemo_type_p(captured->code.val, imemo_iseq));
#endif
        return 1;
    }
    else {
        return 0;
    }
}

static inline int
VM_BH_IFUNC_P(VALUE block_handler)
{
    if ((block_handler & 0x03) == 0x03) {
#if VM_CHECK_MODE > 0
        struct rb_captured_block *captured = (void *)(block_handler & ~0x03);
        VM_ASSERT(imemo_type_p(captured->code.val, imemo_ifunc));
#endif
        return 1;
    }
    else {
        return 0;
    }
}

#define SYMBOL_P(x) RB_SYMBOL_P(x)
#define RB_SYMBOL_P(x) (RB_STATIC_SYM_P(x)||RB_DYNAMIC_SYM_P(x))
#define RB_STATIC_SYM_P(x) (((VALUE)(x)&~((~(VALUE)0)<<RUBY_SPECIAL_SHIFT)) == RUBY_SYMBOL_FLAG)
#define RB_DYNAMIC_SYM_P(x) (!RB_SPECIAL_CONST_P(x) && RB_BUILTIN_TYPE(x) == (RUBY_T_SYMBOL))

VALUE
rb_obj_is_proc(VALUE proc)
{
    if (rb_typeddata_is_kind_of(proc, &proc_data_type)) {
        return Qtrue;
    }
    else {
        return Qfalse;
    }
}
```

以下のように分類できそうです。

- block_handler_type_iseq
  - ポインタのタグの0x01, 0x02のうち、0x01 (`VM_BH_FROM_ISEQ_BLOCK`でセットされる) だけが立っている
  - 多分`struct rb_captured_block *`にキャストしてiseqを取り出せるtype
- block_handler_type_ifunc
  - ポインタのタグの0x01, 0x02のうち、0x01と0x02両方が立っている
  - `struct rb_captured_block`の構造から察するに`const struct vm_ifunc`という奴が入っている
     - `struct vm_ifunc`には任意の関数ポインタと任意のデータとフラグとargcとかが入りそう
- block_handler_type_symbol
  - Symbolオブジェクトっぽい
- block_handler_type_proc
  - Procオブジェクトっぽい

ところで`0x01`とか`0x03`がマジックナンバーすぎて可読性が低めなので、grepしやすいようにそのうち直しておきたいですね…

### block_handler_type_iseqの時にiseqが呼び出される流れ
```c 
static VALUE
vm_invoke_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp, struct rb_calling_info *calling, const struct rb_call_info *ci)
{
    VALUE block_handler = VM_CF_BLOCK_HANDLER(reg_cfp);
    VALUE type = GET_ISEQ()->body->local_iseq->body->type;
    int is_lambda = FALSE;

    if ((type != ISEQ_TYPE_METHOD && type != ISEQ_TYPE_CLASS) ||
        block_handler == VM_BLOCK_HANDLER_NONE) {
        rb_vm_localjump_error("no block given (yield)", Qnil, 0);
    }

  again:
    switch (vm_block_handler_type(block_handler)) {
      case block_handler_type_iseq:
        {
            const struct rb_captured_block *captured = VM_BH_TO_ISEQ_BLOCK(block_handler);
            return vm_invoke_iseq_block(ec, reg_cfp, calling, ci, is_lambda, captured);
        }
      case block_handler_type_ifunc:
        {
            const struct rb_captured_block *captured = VM_BH_TO_IFUNC_BLOCK(block_handler);
            return vm_invoke_ifunc_block(ec, reg_cfp, calling, ci, captured);
        }
      case block_handler_type_proc:
        is_lambda = block_proc_is_lambda(VM_BH_TO_PROC(block_handler));
        block_handler = vm_proc_to_block_handler(VM_BH_TO_PROC(block_handler));
        goto again;
      case block_handler_type_symbol:
        return vm_invoke_symbol_block(ec, reg_cfp, calling, ci, VM_BH_TO_SYMBOL(block_handler));
    }
    VM_UNREACHABLE(vm_invoke_block: unreachable);
    return Qnil;
}

static inline const struct rb_captured_block *
VM_BH_TO_ISEQ_BLOCK(VALUE block_handler)
{
    struct rb_captured_block *captured = VM_TAGGED_PTR_REF(block_handler, 0x03);
    VM_ASSERT(VM_BH_ISEQ_BLOCK_P(block_handler));
    return captured;
}

#define VM_TAGGED_PTR_REF(v, mask) ((void *)((v) & ~mask))

/* ruby iseq -> ruby block */

static VALUE
vm_invoke_iseq_block(rb_execution_context_t *ec, rb_control_frame_t *reg_cfp,
                     struct rb_calling_info *calling, const struct rb_call_info *ci,
                     int is_lambda, const struct rb_captured_block *captured)
{
    const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
    const int arg_size = iseq->body->param.size;
    VALUE * const rsp = GET_SP() - calling->argc;
    int opt_pc = vm_callee_setup_block_arg(ec, calling, ci, iseq, rsp, is_lambda ? arg_setup_method : arg_setup_block);

    SET_SP(rsp);

    vm_push_frame(ec, iseq,
                  VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0),
                  captured->self,
                  VM_GUARDED_PREV_EP(captured->ep), 0,
                  iseq->body->iseq_encoded + opt_pc,
                  rsp + arg_size,
                  iseq->body->local_table_size - arg_size, iseq->body->stack_max);

    return Qundef;
}
```

はい、`captured->code.iseq`が参照されて新しいcontrol frameがpushされました。めでたしめでたし。

## 冒頭の例

既に記事が大分長くなって疲れたが、以下のコードの挙動を追いかけることにする。

```rb  
def a
  b do
    yield
  end
end

def b(&block)
  block.call
end

a { 1 } #=> 1
```

まず、僕の疑問の元になっている理解について整理しておくが、`yield`が呼び出される瞬間以下のようなコールスタックになっていると考えている。

* main
  * #a
     * #b
         * block in #a

で、`invokeblock`が`VM_CF_BLOCK_HANDLER`で`block_handler`を取り出す時、

- "block in #a"のフレームがlepではないと仮定すると
  - `VM_CF_LEP`の際に直近の普通のメソッド呼び出しフレームである`#b`の`cfp->ep`をlepとして取り出し、その`ep[-1]`(`#b`に渡されたブロック)を参照してしまうのではないか
- "block in #a"のフレームがlepであると仮定すると
  - `block.call`でブロックを渡していないので、no block givenになるのではないか

ということを考えている。

まあ実際の挙動から逆算して、以下のことが容易に推察できる

* 予想1: blockを実行するフレームはlepではない (`cfp->ep[0]`に`VM_ENV_FLAG_LOCAL`フラグが立っていない)
* 予想2: "block in #a" のフレームの`cfp->ep[-1]`(前のep)には #b のフレームを飛び越えて #a のフレームのepが入っている

これまでの調査から、mainのフレームの`cfp->block_code`には`{ 1 }`の`blockiseq`が入っており、それを含むcfp後半を差す`struct rb_captured_block *`が #a のフレームの `cfp->ep[-1]`に入っていることが期待でき、また #a の`cfp->ep`はlepのはずなので、「予想1」と「予想2」が両方正しければ`VM_CF_BLOCK_HANDLER`は正しく`struct rb_captured_block *`を`block_handler`として取り出し、そこから`{ 1 }`のiseqを参照できるはずである。

なので、これら2つの予想が正しいことを確認していく。

### blockを実行するフレームはlepか
`Proc#call`の場合について書く。`VM_METHOD_TYPE_OPTIMIZED`かつ`OPTIMIZED_METHOD_TYPE_CALL`で定義されていることを念頭に置きつつ、gdbで追いかけている時に通ったパスを見ておくと、

```c 
static VALUE
vm_call_method_each_type(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    switch (cc->me->def->type) {
      case VM_METHOD_TYPE_ISEQ:
        CI_SET_FASTPATH(cc, vm_call_iseq_setup, TRUE);
        return vm_call_iseq_setup(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_NOTIMPLEMENTED:
      case VM_METHOD_TYPE_CFUNC:
        CI_SET_FASTPATH(cc, vm_call_cfunc, TRUE);
        return vm_call_cfunc(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_ATTRSET:
        CALLER_SETUP_ARG(cfp, calling, ci);
        rb_check_arity(calling->argc, 1, 1);
        cc->aux.index = 0;
        CI_SET_FASTPATH(cc, vm_call_attrset, !((ci->flag & VM_CALL_ARGS_SPLAT) || (ci->flag & VM_CALL_KWARG)));
        return vm_call_attrset(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_IVAR:
        CALLER_SETUP_ARG(cfp, calling, ci);
        rb_check_arity(calling->argc, 0, 0);
        cc->aux.index = 0;
        CI_SET_FASTPATH(cc, vm_call_ivar, !(ci->flag & VM_CALL_ARGS_SPLAT));
        return vm_call_ivar(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_MISSING:
        cc->aux.method_missing_reason = 0;
        CI_SET_FASTPATH(cc, vm_call_method_missing, TRUE);
        return vm_call_method_missing(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_BMETHOD:
        CI_SET_FASTPATH(cc, vm_call_bmethod, TRUE);
        return vm_call_bmethod(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_ALIAS:
        cc->me = aliased_callable_method_entry(cc->me);
        VM_ASSERT(cc->me != NULL);
        return vm_call_method_each_type(ec, cfp, calling, ci, cc);

      case VM_METHOD_TYPE_OPTIMIZED:
        switch (cc->me->def->body.optimize_type) {
          case OPTIMIZED_METHOD_TYPE_SEND:
            CI_SET_FASTPATH(cc, vm_call_opt_send, TRUE);
            return vm_call_opt_send(ec, cfp, calling, ci, cc);
          case OPTIMIZED_METHOD_TYPE_CALL:
            CI_SET_FASTPATH(cc, vm_call_opt_call, TRUE);
            return vm_call_opt_call(ec, cfp, calling, ci, cc);
          default:
            rb_bug("vm_call_method: unsupported optimized method type (%d)",
                   cc->me->def->body.optimize_type);
        }

      case VM_METHOD_TYPE_UNDEF:
        break;

      case VM_METHOD_TYPE_ZSUPER:
        return vm_call_zsuper(ec, cfp, calling, ci, cc, RCLASS_ORIGIN(cc->me->owner));

      case VM_METHOD_TYPE_REFINED: {
        const rb_cref_t *cref = rb_vm_get_cref(cfp->ep);
        VALUE refinements = cref ? CREF_REFINEMENTS(cref) : Qnil;
        VALUE refinement;
        const rb_callable_method_entry_t *ref_me;

        refinement = find_refinement(refinements, cc->me->owner);

        if (NIL_P(refinement)) {
            goto no_refinement_dispatch;
        }
        ref_me = rb_callable_method_entry(refinement, ci->mid);

        if (ref_me) {
            if (cc->call == vm_call_super_method) {
                const rb_control_frame_t *top_cfp = current_method_entry(ec, cfp);
                const rb_callable_method_entry_t *top_me = rb_vm_frame_method_entry(top_cfp);
                if (top_me && rb_method_definition_eq(ref_me->def, top_me->def)) {
                    goto no_refinement_dispatch;
                }
            }
            cc->me = ref_me;
            if (ref_me->def->type != VM_METHOD_TYPE_REFINED) {
                return vm_call_method(ec, cfp, calling, ci, cc);
            }
        }
        else {
            cc->me = NULL;
            return vm_call_method_nome(ec, cfp, calling, ci, cc);
        }

      no_refinement_dispatch:
        if (cc->me->def->body.refined.orig_me) {
            cc->me = refined_method_callable_without_refinement(cc->me);
        }
        else {
            VALUE klass = RCLASS_SUPER(cc->me->defined_class);
            cc->me = klass ? rb_callable_method_entry(klass, ci->mid) : NULL;
        }
        return vm_call_method(ec, cfp, calling, ci, cc);
      }
    }

    rb_bug("vm_call_method: unsupported method type (%d)", cc->me->def->type);
}

static VALUE
vm_call_opt_call(rb_execution_context_t *ec, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    rb_proc_t *proc;
    int argc;
    VALUE *argv;

    CALLER_SETUP_ARG(cfp, calling, ci);

    argc = calling->argc;
    argv = ALLOCA_N(VALUE, argc);
    GetProcPtr(calling->recv, proc);
    MEMCPY(argv, cfp->sp - argc, VALUE, argc);
    cfp->sp -= argc + 1;

    return rb_vm_invoke_proc(ec, proc, argc, argv, calling->block_handler);
}

VALUE
rb_vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc,
                  int argc, const VALUE *argv, VALUE passed_block_handler)
{
    VALUE self = vm_block_self(&proc->block);
    vm_block_handler_verify(passed_block_handler);

    if (proc->is_from_method) {
        return vm_invoke_bmethod(ec, proc, self, argc, argv, passed_block_handler);
    }
    else {
        return vm_invoke_proc(ec, proc, self, argc, argv, passed_block_handler);
    }
}

static VALUE
vm_invoke_proc(rb_execution_context_t *ec, rb_proc_t *proc, VALUE self,
               int argc, const VALUE *argv, VALUE passed_block_handler)
{
    VALUE val = Qundef;
    enum ruby_tag_type state;
    volatile int stored_safe = ec->safe_level;

    EC_PUSH_TAG(ec);
    if ((state = EC_EXEC_TAG()) == TAG_NONE) {
        ec->safe_level = proc->safe_level;
        val = invoke_block_from_c_proc(ec, proc, self, argc, argv, passed_block_handler, proc->is_lambda);
    }
    EC_POP_TAG();

    ec->safe_level = stored_safe;

    if (state) {
        EC_JUMP_TAG(ec, state);
    }
    return val;
}

static inline VALUE
invoke_block_from_c_proc(rb_execution_context_t *ec, const rb_proc_t *proc,
                         VALUE self, int argc, const VALUE *argv,
                         VALUE passed_block_handler, int is_lambda)
{
    const struct rb_block *block = &proc->block;

  again:
    switch (vm_block_type(block)) {
      case block_type_iseq:
        return invoke_iseq_block_from_c(ec, &block->as.captured, self, argc, argv, passed_block_handler, NULL, is_lambda);
      case block_type_ifunc:
        return vm_yield_with_cfunc(ec, &block->as.captured, self, argc, argv, passed_block_handler);
      case block_type_symbol:
        return vm_yield_with_symbol(ec, block->as.symbol, argc, argv, passed_block_handler);
      case block_type_proc:
        is_lambda = block_proc_is_lambda(block->as.proc);
        block = vm_proc_block(block->as.proc);
        goto again;
    }
    VM_UNREACHABLE(invoke_block_from_c_proc);
    return Qundef;
}

static inline VALUE
invoke_iseq_block_from_c(rb_execution_context_t *ec, const struct rb_captured_block *captured,
                         VALUE self, int argc, const VALUE *argv, VALUE passed_block_handler,
                         const rb_cref_t *cref, int is_lambda)
{
    const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
    int i, opt_pc;
    VALUE type = VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0);
    rb_control_frame_t *cfp = ec->cfp;
    VALUE *sp = cfp->sp;
    const rb_callable_method_entry_t *me = ec->passed_bmethod_me;
    ec->passed_bmethod_me = NULL;
    stack_check(ec);

    CHECK_VM_STACK_OVERFLOW(cfp, argc);
    cfp->sp = sp + argc;
    for (i=0; i<argc; i++) {
        sp[i] = argv[i];
    }

    opt_pc = vm_yield_setup_args(ec, iseq, argc, sp, passed_block_handler,
                                 (is_lambda ? arg_setup_method : arg_setup_block));
    cfp->sp = sp;

    if (me == NULL) {
        return invoke_block(ec, iseq, self, captured, cref, type, opt_pc);
    }
    else {
        return invoke_bmethod(ec, iseq, self, captured, me, type, opt_pc);
    }
}

/* C -> Ruby: block */

static inline VALUE
invoke_block(rb_execution_context_t *ec, const rb_iseq_t *iseq, VALUE self, const struct rb_captured_block *captured, const rb_cref_t *cref, VALUE type, int opt_pc)
{
    int arg_size = iseq->body->param.size;

    vm_push_frame(ec, iseq, type | VM_FRAME_FLAG_FINISH, self,
                  VM_GUARDED_PREV_EP(captured->ep),
                  (VALUE)cref, /* cref or method */
                  iseq->body->iseq_encoded + opt_pc,
                  ec->cfp->sp + arg_size,
                  iseq->body->local_table_size - arg_size,
                  iseq->body->stack_max);
    return vm_exec(ec);
}
```

typeは`VM_FRAME_MAGIC_BLOCK | (is_lambda ? VM_FRAME_FLAG_LAMBDA : 0) | VM_FRAME_FLAG_FINISH`っぽい。`VM_ENV_FLAG_LOCAL`がないのでこれはlepではなさそう。

### blockのフレームの`cfp->ep[-1]`にはどのフレームの`ep`が入るか

`vm_push_frame`第五引数`specval`が入るわけだけど、↑のコードを見ると`VM_GUARDED_PREV_EP(captured->ep)`という奴が渡されるように見える。

```c 
#define VM_GUARDED_PREV_EP(ep)         GC_GUARDED_PTR(ep)
#define GC_GUARDED_PTR(p)     VM_TAGGED_PTR_SET((p), 0x01)
#define VM_TAGGED_PTR_SET(p, tag)  ((VALUE)(p) | (tag))
```

`VM_GUARDED_PREV_EP`は`captured->ep`に`0x01`のタグ打ってるだけだった。

`captured`は`invoke_block_from_c_proc`内で`&block->as.captured`として作られている模様。その`block`は`&proc->block`で、その`proc`(`rb_proc_t`)は`vm_call_opt_call`内で`GetProcPtr(calling->recv, proc);`で作られてそう。

```c 
#define GetProcPtr(obj, ptr) \
  GetCoreDataFromValue((obj), rb_proc_t, (ptr))

#define GetCoreDataFromValue(obj, type, ptr) ((ptr) = CoreDataFromValue((obj), type))
#define CoreDataFromValue(obj, type) (type*)DATA_PTR(obj)
#define DATA_PTR(dta) (RDATA(dta)->data)
#define RDATA(obj)   (R_CAST(RData)(obj))
#define R_CAST(st)   (struct st*)

struct RData {
    struct RBasic basic;
    void (*dmark)(void*);
    void (*dfree)(void*);
    void *data;
};
```

つまり`GetProcPtr(calling->recv, proc)`というのは`proc = (rb_proc_t*)(((struct RData*)(calling->recv))->data)`なんだけど、この`calling`は`block.call`時のものなので`Proc`オブジェクトがおそらく入っている。

でまあ例のコードがブロックパラメータを取っているので、ブロックパラメータのProcオブジェクトがどう作られるかを見ておく。

#### ブロックパラメータのProcオブジェクトはどう作られるか
今更だが今回のコードのISeqをダンプしておく

```
== disasm: #<ISeq:<main>@a.rb:0>========================================
== catch table
| catch type: break  st: 0020 ed: 0025 sp: 0000 cont: 0025
== disasm: #<ISeq:block in <main>@a.rb:11>==============================
== catch table
| catch type: redo   st: 0001 ed: 0002 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0002 sp: 0000 cont: 0002
|------------------------------------------------------------------------
0000 nop                                                              (  11)[Bc]
0001 putobject_OP_INT2FIX_O_1_C_ [Li]
0002 leave            [Br]
|------------------------------------------------------------------------
0000 putspecialobject 1                                               (   1)[Li]
0002 putobject        :a
0004 putiseq          a
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 pop
0010 putspecialobject 1                                               (   7)[Li]
0012 putobject        :b
0014 putiseq          b
0016 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0019 pop
0020 putself                                                          (  11)[Li]
0021 send             <callinfo!mid:a, argc:0, FCALL>, <callcache>, block in <main>
0025 leave
== disasm: #<ISeq:a@a.rb:1>=============================================
== catch table
| catch type: break  st: 0000 ed: 0005 sp: 0000 cont: 0005
== disasm: #<ISeq:block in a@a.rb:2>====================================
== catch table
| catch type: redo   st: 0001 ed: 0003 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0003 sp: 0000 cont: 0003
|------------------------------------------------------------------------
0000 nop                                                              (   2)[Bc]
0001 invokeblock      <callinfo!argc:0, ARGS_SIMPLE>                  (   3)[Li]
0003 leave            [Br]
|------------------------------------------------------------------------
0000 putself                                                          (   2)[LiCa]
0001 send             <callinfo!mid:b, argc:0, FCALL>, <callcache>, block in a
0005 leave            [Re]
== disasm: #<ISeq:b@a.rb:7>=============================================
local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: 0, kw: -1@-1, kwrest: -1])
[ 1] block<Block>
0000 getblockparam    block, 0                                        (   8)[LiCa]
0003 opt_send_without_block <callinfo!mid:call, argc:0, ARGS_SIMPLE>, <callcache>
0006 leave            [Re]
```

getblockparamが入ったのはこのコミット: https://github.com/ruby/ruby/commit/5ee9513a7104078d9d2f51aecc354ae67f1ba002

`VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM`というのはそのフレームで`getblockparam`や`setblockparam`が一度でも実行されるとセットされる奴なので、最初は`rb_vm_bh_to_procval(th, VM_ENV_BLOCK_HANDLER(ep))`がブロックパラメータ取得に使われることがわかる。

`0000 getblockparam    block, 0`の通りlevelが0なので、`VM_ENV_BLOCK_HANDLER(ep)`の`ep`は #b のフレームのepということになる。

```c 
static inline VALUE
VM_ENV_BLOCK_HANDLER(const VALUE *ep)
{
    VM_ASSERT(VM_ENV_LOCAL_P(ep));
    return ep[VM_ENV_DATA_INDEX_SPECVAL];
}

#define VM_ENV_DATA_INDEX_SPECVAL    (-1) /* ep[-1] */

VALUE
rb_vm_bh_to_procval(const rb_execution_context_t *ec, VALUE block_handler)
{
    if (block_handler == VM_BLOCK_HANDLER_NONE) {
        return Qnil;
    }
    else {
        switch (vm_block_handler_type(block_handler)) {
          case block_handler_type_iseq:
          case block_handler_type_ifunc:
            return rb_vm_make_proc(ec, VM_BH_TO_CAPT_BLOCK(block_handler), rb_cProc);
          case block_handler_type_symbol:
            return rb_sym_to_proc(VM_BH_TO_SYMBOL(block_handler));
          case block_handler_type_proc:
            return VM_BH_TO_PROC(block_handler);
          default:
            VM_UNREACHABLE(rb_vm_bh_to_procval);
        }
    }
}

static inline const struct rb_captured_block *
VM_BH_TO_CAPT_BLOCK(VALUE block_handler)
{
    struct rb_captured_block *captured = VM_TAGGED_PTR_REF(block_handler, 0x03);
    VM_ASSERT(VM_BH_IFUNC_P(block_handler) || VM_BH_ISEQ_BLOCK_P(block_handler));
    return captured;
}

static inline VALUE
rb_vm_make_proc(const rb_execution_context_t *ec, const struct rb_captured_block *captured, VALUE klass)
{
    return rb_vm_make_proc_lambda(ec, captured, klass, 0);
}

VALUE
rb_vm_make_proc_lambda(const rb_execution_context_t *ec, const struct rb_captured_block *captured, VALUE klass, int8_t is_lambda)
{
    VALUE procval;

    if (!VM_ENV_ESCAPED_P(captured->ep)) {
        rb_control_frame_t *cfp = VM_CAPTURED_BLOCK_TO_CFP(captured);
        vm_make_env_object(ec, cfp);
    }
    VM_ASSERT(VM_EP_IN_HEAP_P(ec, captured->ep));
    VM_ASSERT(imemo_type_p(captured->code.val, imemo_iseq) ||
              imemo_type_p(captured->code.val, imemo_ifunc));

    procval = vm_proc_create_from_captured(klass, captured,
                                           imemo_type(captured->code.val) == imemo_iseq ? block_type_iseq : block_type_ifunc,
                                           (int8_t)ec->safe_level, FALSE, is_lambda);
    return procval;
}

static VALUE
vm_proc_create_from_captured(VALUE klass,
                             const struct rb_captured_block *captured,
                             enum rb_block_type block_type,
                             int8_t safe_level, int8_t is_from_method, int8_t is_lambda)
{
    VALUE procval = rb_proc_alloc(klass);
    rb_proc_t *proc = RTYPEDDATA_DATA(procval);

    VM_ASSERT(VM_EP_IN_HEAP_P(GET_EC(), captured->ep));

    /* copy block */
    RB_OBJ_WRITE(procval, &proc->block.as.captured.self, captured->self);
    RB_OBJ_WRITE(procval, &proc->block.as.captured.code.val, captured->code.val);
    rb_vm_block_ep_update(procval, &proc->block, captured->ep);

    vm_block_type_set(&proc->block, block_type);
    proc->safe_level = safe_level;
    proc->is_from_method = is_from_method;
    proc->is_lambda = is_lambda;

    return procval;
}

void
rb_vm_block_ep_update(VALUE obj, const struct rb_block *dst, const VALUE *ep)
{
    *((const VALUE **)&dst->as.captured.ep) = ep;
    RB_OBJ_WRITTEN(obj, Qundef, VM_ENV_ENVVAL(ep));
}
```

`rb_proc_alloc`でProcオブジェクトが作られる。`struct RData`として見た時のdataフィールドは`vm_proc_create_from_captured`内の`rb_proc_t *proc`になるだろう。

`rb_vm_block_ep_update`内で`captured->ep`を`&proc->block.as.captured.ep`にセットしているから、blockのフレームの`cfp->ep[-1]`にはこの`captured->ep`が入るはずである。

で、`rb_vm_block_ep_update`内での`captured->ep`が何かについて考える。`captured`は`VM_BH_TO_CAPT_BLOCK`内で`VM_TAGGED_PTR_REF(block_handler, 0x03)`によって`block_handler`から取り出されたものであるから、これは`VM_ENV_BLOCK_HANDLER`内で #b のフレームのepから`ep[-1]`として作られたものである。ここにはこれまでの調査から #a のcfpの後半を指す`struct rb_captured_block *`が入っている(かつその`code.iseq`に`blockiseq`が入っている)はずで、つまりそこから`captured->ep`を参照すると #a のフレームのepが入っていることになる。

まとめると、挙動はgetblockparamのlevel次第ではあるが、少なくともlevel=0の場合は、ブロックパラメータを引数に持つフレームのepから`ep[-1]`を参照した`captured`の`captured->ep`がブロックパラメータから作られるProcオブジェクトの`data->block.as.captured.ep`となり、かつそれは #a のフレームのepになっているので、block in #aのフレームの`ep[-1]`には #a のフレームのepがセットされるということだ。

なので、block in #a のフレームのcfpから`VM_CF_LEP(cfp)`をやると #a のフレームのepが取得され、その`ep[-1]`から #a に渡されたブロックのiseqが参照できる `struct rb_captured_block *`が参照できるというわけである。

# その他
長くなったので一旦この記事を保存するが、後で以下の内容を追記するかもしれない

* ブロックのISeqがどのように `Proc#call` で実行されるかの詳細
* `proc do ... end` を呼び出した時の環境や元のepはどこに保存されるか