Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

背景

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をいじっていたら以下のコードが動かなくなりました。

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のスクリプトをいくつか適当に実行してみれば理解できる。この見出しがそれによって導き出された結論である。

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

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

a #=> 1

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

一方、

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ではできるのか?

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に渡されるか

一番簡単なケース

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で実行されることを一応確認。

invokeblockblock_handler をどのように見つけるか

/**
  @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();
    }
}
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->epepとし、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が立つか

前に書いた記事にある程度書いてあった。

  • 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 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);
}
#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になっていることは以前の記事で解説したので割愛。

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

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

send命令でcalling->block_handlerはどのように作られるか

send命令のコードを見ると、名前的にvm_caller_setup_arg_blockが怪しい。

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から確認できるようになっている。フラグは以下のものがセットされる

#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"っぽい。

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

    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);
    }
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 | 0x01block_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の中身の定義を見てみる。

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_handlervm_push_framespecvalとして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を知る必要がある。

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が呼び出される流れ

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されました。めでたしめでたし。

冒頭の例

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

def a
  b do
    yield
  end
end

def b(&block)
  block.call
end

a { 1 } #=> 1

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

  • main
    • #a
      • #b
        • block in #a

で、invokeblockVM_CF_BLOCK_HANDLERblock_handlerを取り出す時、

  • "block in #a"のフレームがlepではないと仮定すると
    • VM_CF_LEPの際に直近の普通のメソッド呼び出しフレームである#bcfp->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で追いかけている時に通ったパスを見ておくと、

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)という奴が渡されるように見える。

#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_EPcaptured->ep0x01のタグ打ってるだけだった。

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

#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)なんだけど、このcallingblock.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というのはそのフレームでgetblockparamsetblockparamが一度でも実行されるとセットされる奴なので、最初は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ということになる。

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が何かについて考える。capturedVM_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.iseqblockiseqが入っている)はずで、つまりそこからcaptured->epを参照すると #a のフレームのepが入っていることになる。

まとめると、挙動はgetblockparamのlevel次第ではあるが、少なくともlevel=0の場合は、ブロックパラメータを引数に持つフレームのepからep[-1]を参照したcapturedcaptured->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はどこに保存されるか
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away