rubyでgroovyやKotlinのitが使えるthats_it gemを作った

## 追記: Ruby 2.7からはこのgemは必要ないです

Ruby 2.7でNumbered parametersが追加されました。最高!
http://bugs.ruby-lang.org/issues/4475

```
% ruby -ve 'p [1,2,3].map { @1 * 2 }'
ruby 2.7.0dev (2019-03-18 trunk 67289) [x86_64-darwin17]
[2, 4, 6]
```

## TL;DR
groovyやKotlinの`it`のようにブロックに渡された値を`it`で参照できる`thats_it`gemの紹介です。

```rb
require "thats_it"

[1, 2, 3].map { it * 2 }
# => [2, 4, 6]
```

便利ですね。

了

## Kotlinやgroovyのitとは

ラムダ式が1つしかパラメーターを取らない場合、そのパラメーターを宣言しなくても暗黙的に宣言された「it」変数でそのパラメーターを参照できる便利機能。

### 参考

[Higher-Order Functions and Lambdas - Kotlin Programming Language it: implicit name of a single parameter][kotlin]
[The Apache Groovy programming language - Closures 2.2. Implicit parameter][groovy]

[kotlin]: https://kotlinlang.org/docs/reference/lambdas.html#it-implicit-name-of-a-single-parameter
[groovy]: http://groovy-lang.org/closures.html#implicit-it

## 忙しい人向けの実装方法解説

ブロックが取る引数が0かつ`it`の呼び出しがある場合にブロックのISeqを書き換えてブロックが1つ引数を取ることにする。[^いいわけ]

[^いいわけ]: 実際には書き換え後のコードと同様の動きをするわけではない。ローカル変数`it`には何も入っておらず`it`は変数参照ではなくメソッド呼び出しとなっている。RubyVMとISeqの動作はRubyのコードで表現できないので雰囲気だけ掴んでもらえれば。

```rb
# 書き換え前
1.yield_self { it }
# 書き換え後
1.yield_self {|it| it }
```

了

## どのタイミングでブロックを書き換えるか

TracePointを使いメソッド呼び出しのタイミングでメソッドに渡されたブロックのISeqを取得し書き換える。

```rb
# Rubyで書かれたメソッドの呼び出し時に呼ばれる
TracePoint.trace(:call) do |tp|
  # ここで書き換える
end

# Cで書かれたメソッドの呼び出し時に呼ばれる
TracePoint.trace(:c_call) do |tp|
  # ここで書き換える
end
```

ブロックが呼び出されるタイミングで呼び出される`b_call`というイベントもあるがそこではISeqを書き換えられない。理由は後述。

## ブロックが持っている情報

ブロックのISeqにはブロックが取る引数の情報が保持されている。
以下のような引数を取る場合のISeqを確認してみる。

```rb
42.yield_self {|it| it }
```

rubyではオプションで`--dump=insns`を渡すとISeqを確認できる。

```
% ruby -v --dump=insns -e '42.yield_self {|it| it }'
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,24)>=============================
== catch table
| catch type: break  st: 0000 ed: 0006 sp: 0000 cont: 0006
== disasm: #<ISeq:block in <main>@-e:1 (1,14)-(1,24)>===================
== catch table
| catch type: redo   st: 0001 ed: 0003 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0003 sp: 0000 cont: 0003
|------------------------------------------------------------------------
local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 1] it<Arg>
0000 nop                                                              (   1)[Bc]
0001 getlocal_OP__WC__0 it[Li]
0003 leave            [Br]
|------------------------------------------------------------------------
0000 putobject        42                                              (   1)[Li]
0002 send             <callinfo!mid:yield_self, argc:0>, <callcache>, block in <main>
0006 leave
```

ブロックのISeqを確認してみると以下のように引数を1つ取ることが分かる。

> local table (size: 1, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])

一方ブロックが引数を取らない場合はローカル変数が無いので以下のようにlocal tableが省略して表示される。
表示されていないだけで、実際にはlocal tableの情報は引数を渡さない場合もブロックのISeqに保持されており、`size: 0, argc: 0`となっている。

```
% ruby -v --dump=insns -e '42.yield_self { it }'
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,20)>=============================
== catch table
| catch type: break  st: 0000 ed: 0006 sp: 0000 cont: 0006
== disasm: #<ISeq:block in <main>@-e:1 (1,14)-(1,20)>===================
== catch table
| catch type: redo   st: 0001 ed: 0005 sp: 0000 cont: 0001
| catch type: next   st: 0001 ed: 0005 sp: 0000 cont: 0005
|------------------------------------------------------------------------
0000 nop                                                              (   1)[Bc]
0001 putself          [Li]
0002 opt_send_without_block <callinfo!mid:it, argc:0, FCALL|VCALL|ARGS_SIMPLE>, <callcache>
0005 leave            [Br]
|------------------------------------------------------------------------
0000 putobject        42                                              (   1)[Li]
0002 send             <callinfo!mid:yield_self, argc:0>, <callcache>, block in <main>
0006 leave
```

## ブロック呼び出し周りのRubyVMの動作

ブロックを呼び出すコードを読めばブロックのISeqを取る方法やどう書き換えればいいかがが分かる。

以下のようなブロックを呼び出す内容のプログラムのISeqを確認する。

```rb
def foo
  yield 42
end
```

```
% ruby -v --dump=insns -e 'def foo; yield 42; end'
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,22)>=============================
0000 putspecialobject 1                                               (   1)[Li]
0002 putobject        :foo
0004 putiseq          foo
0006 opt_send_without_block <callinfo!mid:core#define_method, argc:2, ARGS_SIMPLE>, <callcache>
0009 leave
== disasm: #<ISeq:foo@-e:1 (1,0)-(1,22)>================================
0000 putobject        42                                              (   1)[LiCa]
0002 invokeblock      <callinfo!argc:1, ARGS_SIMPLE>
0004 leave            [Re]
```

`invokeblock`命令で呼び出しているよう。ruby/rubyをcloneしてgit grepする[^ruby/ruby]。

```
% git grep invokeblock
bootstraptest/test_insns.rb:  [ 'invokeblock',            <<~'},', ], # {
compile.c:	ADD_INSN1(ret, line, invokeblock, new_callinfo(iseq, 0, FIX2INT(argc), flag, keywords, FALSE));
doc/ChangeLog-1.9.3:	* insns.def (invokeblock): fix of splat argument.
doc/ChangeLog-1.9.3:	* insns.def (invokeblock): check block is created by lambda
doc/ChangeLog-2.0.0:	* insns.def (send, invokesuper, invokeblock, opt_*), vm_core.h:
insns.def:invokeblock
```

YARVの命令は`insns.def`ファイルで定義されているので`insns.def`の`invokeblock`を読む。[^yarvarch]

[^ruby/ruby]: https://github.com/ruby/ruby/tree/v2_5_1
[^yarvarch]: RubyVMの設計についてのドキュメント参考 https://github.com/ruby/ruby/blob/v2_5_1/template/yarvarch.ja

### invokeblock

コメントから見てわかるように`yield`に対応している。
`vm_invoke_block`を実行してますね。
この辺でgdbで止めてデバッグします。

```c:insns.def
/**
  @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();
    }
}
```
https://github.com/ruby/ruby/blob/v2_5_1/insns.def#L938-L959

### vm_invoke_block

ブロックとして渡された引数があるか確認、ブロックが渡されてなければno block givenの例外を投げる。
ふつうにrubyで`yield`した際は`vm_invoke_iseq_block`が呼ばれるようです。

```vm_insnhelper.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;
}
```
https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L2695-L2728

### vm_invoke_iseq_block

引数周りのスタックを調整してからYARVのcontrol frameを積んでる。

```vm_insnhelper.c
/* 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;
}
```
https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L2623-L2646


`struct rb_captured_block *`からブロックのISeqが取得できることが分かりました。
`vm_push_frame`まで見ます。

```vm_insnhelper.c
    const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
```

### vm_push_frame

`vm_push_frame`のシグネチャは以下

```vm_insnhelper.c
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)
```
https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L199-L209

`vm_push_frame`の後ろから3番目の引数がspになっているのがわかる。`sp`には`rsp + arg_size`が入るよう。

```vm_insnhelper.c
    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);
```

`rsp`と`arg_size`は以下のように計算されていますね。

```vm_insnhelper.c
    const int arg_size = iseq->body->param.size;
    VALUE * const rsp = GET_SP() - calling->argc;
```

`arg_size`にはブロックのISeqの引数の数の情報が入る。
ブロックの引数を書かない場合ここの`arg_size`が`0`になる。
`calling->argc`には`yield`に渡した引数の数が入っている、なので`1`。
`calling`の型は`struct rb_calling_info *`で対応するのはこの部分かな?

```
0002 invokeblock      <callinfo!argc:1, ARGS_SIMPLE>
```

`rsp`には現在のスタックポインターから呼び出し側で`yield`に渡して積んだ引数を引いた部分が入る。
つまり引数0のブロックを呼び出す場合いくら`yield`に引数を渡してもブロックの実行前に破棄されてしまうよう。[^argc]

[^argc]: 例えば呼び出す際に`yield 1, 2, 3`すると`calling->argc`は`3`、なのでスタックに3つ値が積まれた状態だと`rsp`は積まれていない底を指している。新しいスタックの底は`rsp + arg_size`で決定されるので引数0の場合は積んである1, 2, 3は後からスタックを積む際に上書きされて消えてしまう。ブロックが3つ引数を取る場合は`rsp + 3`で3つの値が積まれた状態を指すようになる。このへん説明難しいのでRubyのしくみを読みましょう...

## ブロック呼び出し時の動作雑な理解

- cfpからblock_handlerが取れる
  - block_handlerは4種類ある
     - iseq
     - ifunc
     - proc
     - symbol
- iseqのblock_handlerからはiseqが取れる
- `iseq->body->param.size`が0だと`yield 1, 2, 3, 4, ...`といくら`yield`に引数を渡しても捨てられてしまう

## b_callでブロックのISeqを書き換えても意味なさ

以下のようなプログラムをgdbで実行します。

```rb
TracePoint.trace(:b_call) do |tp|
  1.yield_self
end

def foo
  yield 42
end

foo {}
```

`vm_invoke_iseq_block`と`rb_obj_yield_self`にbreakpointを仕掛けて呼び出される順序を確認。

```
% gdb $(rbenv which ruby)
GNU gdb (Gentoo 8.1 p1) 8.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.gentoo.org/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /home/sei/.rbenv/versions/2.5.1/bin/ruby...done.
(gdb) break vm_invoke_iseq_block if rb_vm_top_self() == calling->recv
Breakpoint 1 at 0x1850fd: file vm_insnhelper.c, line 2630.
(gdb) break rb_obj_yield_self
Breakpoint 2 at 0x909da: file object.c, line 577.
(gdb) run --disable-gems -e 'TracePoint.trace(:b_call) {|tp| 1.yield_self }; def foo; yield 42; end; foo
{}'
Starting program: /home/sei/.rbenv/versions/2.5.1/bin/ruby --disable-gems -e 'TracePoint.trace(:b_call) {|tp| 1.yield_self }; def foo; yield 42; end; foo {}'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff7ff6700 (LWP 15626)]
Error in testing breakpoint condition:
Couldn't get registers: そのようなプロセスはありません.
An error occurred while in a function called from GDB.
Evaluation of the expression containing the function
(rb_vm_top_self) will be abandoned.
When the function is done executing, GDB will silently stop.
Selected thread is running.
(gdb) frame 0
#0  vm_invoke_iseq_block (ec=0x555555a79258, reg_cfp=0x7ffff7fc5f80, calling=0x7fffffffd8b0,
    ci=0x555555babf60, is_lambda=0, captured=0x7ffff7fc5fc8) at vm_insnhelper.c:2630
2630        const rb_iseq_t *iseq = rb_iseq_check(captured->code.iseq);
(gdb) c
Continuing.

Thread 1 "ruby" hit Breakpoint 2, rb_obj_yield_self (obj=3) at object.c:577
577         RETURN_SIZED_ENUMERATOR(obj, 0, 0, rb_obj_size);
(gdb) frame 0
#0  rb_obj_yield_self (obj=3) at object.c:577
577         RETURN_SIZED_ENUMERATOR(obj, 0, 0, rb_obj_size);
(gdb) c
Continuing.
[Thread 0x7ffff7ff6700 (LWP 15626) exited]
[Inferior 1 (process 15622) exited normally]
```

見ての通り`vm_invoke_block`が呼ばれてから`rb_obj_yield_self`が呼び出されている。

1. `yield`の引数をスタックに積む
2. `invokeblock`が呼ばれる
3. `vm_invoke_block`が呼ばれる
4. `vm_invoke_iseq_block`が呼ばれる
5. `vm_push_frame`が呼ばれる(このタイミングでスタックに積んだ引数が捨てられる)
6. TracePointの`b_call`が呼ばれる

6の段階でISeqを書き換えても5の段階で既に`yield`に渡された値がすてられてしまう。
なので書き換えるなら4を呼ぶ前にISeqを書き換えないといけない。

6の段階のcontrol frameのスタックを見てみると以下のようになっている。
0006がyield_selfの呼び出しで、0005がTracePoint作成時に渡したブロック、0004がfooに渡したブロックとなっており、既に制御フレームが積まれているのが分かる。

```
(gdb) call rb_vmdebug_stack_dump_raw_current()
-- Control frame information -----------------------------------------------
c:0006 p:---- s:0021 e:000020 CFUNC  :yield_self
c:0005 p:0005 s:0017 e:000016 BLOCK  -e:1 [FINISH]
c:0004 p:0001 s:0013 e:000012 BLOCK  -e:1
c:0003 p:0004 s:0010 e:000009 METHOD -e:1
c:0002 p:0029 s:0006 E:002660 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0003 E:000460 (none) [FINISH]
```

## ブロックのISeqをメソッド呼び出し時に取得する

以下のようなコードをgdbで実行して`rb_obj_yield_self`にbreakポイントを仕掛けブロックのISeqを取る方法を確認する。

```rb
TracePoint.trace(:call) do |tp|
  1.yield_self
end

def foo
  yield 42
end

foo { 42 }
```

とりあえず止まった

```
% gdb $(rbenv which ruby)
GNU gdb (Gentoo 8.1 p1) 8.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://bugs.gentoo.org/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from /home/sei/.rbenv/versions/2.5.1/bin/ruby...done.
(gdb) source ~/src/github.com/ruby/ruby/.gdbinit
(gdb) break rb_obj_yield_self
Breakpoint 1 at 0x909da: file object.c, line 577.
(gdb) run --disable-gems -e 'TracePoint.trace(:call) {|tp| 1.yield_self }; def foo; yield 42; end; foo { 42 }
'
Starting program: /home/sei/.rbenv/versions/2.5.1/bin/ruby --disable-gems -e 'TracePoint.trace(:call) {|tp| 1.yield_self }; def foo; yield 42; end; foo {}'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff7ff6700 (LWP 16662)]

Thread 1 "ruby" hit Breakpoint 1, rb_obj_yield_self (obj=3) at object.c:577
577         RETURN_SIZED_ENUMERATOR(obj, 0, 0, rb_obj_size);
```

メソッドは2つ上のフレームのよう

```
(gdb) call rb_vmdebug_stack_dump_raw_current()
-- Control frame information -----------------------------------------------
c:0005 p:---- s:0018 e:000017 CFUNC  :yield_self
c:0004 p:0005 s:0014 e:000013 BLOCK  -e:1 [FINISH]
c:0003 p:0001 s:0010 e:000009 METHOD -e:1
c:0002 p:0029 s:0006 E:002660 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0003 E:000460 (none) [FINISH]
```

今のcontrol frameは`ruby_current_execution_context_ptr`で取れる。[^rb_p]

[^rb_p]: Rubyのオブジェクトをプリント出来るデバッグ用のC関数 https://github.com/ruby/ruby/blob/v2_5_1/io.c#L7655

```
(gdb) p *(ruby_current_execution_context_ptr->cfp)
$40 = {pc = 0x0, sp = 0x7ffff7ec60a0, iseq = 0x0, self = 3, ep = 0x7ffff7ec6098, block_code = 0x0}
(gdb) call rb_p((ruby_current_execution_context_ptr->cfp)->self)
1
```

cfpは`--`して伸びる。試しに-したCFPを見ると使われてない空のフレームがあるのが分かる。

```
(gdb) p *(ruby_current_execution_context_ptr->cfp - 1)
$49 = {pc = 0x0, sp = 0x7ffff7ec60b8, iseq = 0x0, self = 3, ep = 0x7ffff7ec60b0, block_code = 0x0}
(gdb) p *(ruby_current_execution_context_ptr->cfp - 2)
$50 = {pc = 0x0, sp = 0x0, iseq = 0x0, self = 0, ep = 0x0, block_code = 0x0}
(gdb) p *(ruby_current_execution_context_ptr->cfp - 3)
$51 = {pc = 0x0, sp = 0x0, iseq = 0x0, self = 0, ep = 0x0, block_code = 0x0}
```

なので2つ上のメソッド呼び出しのcfpを見るときは`+ 2`すると見れる。

```
(gdb) p *(ruby_current_execution_context_ptr->cfp + 2)
$23 = {pc = 0x555555babee8, sp = 0x7ffff7ec6060, iseq = 0x555555a95d80, self = 93824998023880,
  ep = 0x7ffff7ec6058, block_code = 0x0}
```

さきほど見た`vm_invoke_block`でブロックハンドラーを取得していた関数`VM_CF_BLOCK_HANDLER`を呼んでみる。

```
(gdb) p VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2))
$52 = 140737353899977
```

`vm_invoke_block`同様`VM_BH_TO_ISEQ_BLOCK`を呼んで`struct rb_captured_block *`に変換する。

```
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))
$54 = (const struct rb_captured_block *) 0x7ffff7fc5fc8
(gdb) p *VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))
$55 = {self = 93824998023880, ep = 0x555555bb0090, code = {iseq = 0x555555a95c90,
    ifunc = 0x555555a95c90, val = 93824997743760}}
```

`vm_invoke_block_iseq`のようにiseqにアクセスしてみる。

```
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq
$56 = (const rb_iseq_t *) 0x555555a95c90
(gdb) p *VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq
$57 = {flags = 28698, reserved1 = 0, body = 0x555555babfb0, aux = {compile_data = 0x8, loader = {
      obj = 8, index = 0}, trace_events = 8}}
```

このままだとよくわからないのでdisasmした結果を見てみる。

```
(gdb) call rb_p(rb_iseq_disasm(VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq))
"== disasm: #<ISeq:block in <main>@-e:1 (1,74)-(1,80)>===================\n== catch table\n| catch type: redo   st: 0001 ed: 0003 sp: 0000 cont: 0001\n| catch type: next   st: 0001 ed: 0003 sp: 0000 cont: 0003\n|------------------------------------------------------------------------\n0000 nop                                                              (   1)[Bc]\n0001 putobject        42[Li]\n0003 leave            [Br]\n"
```

目的のブロックが取れている。

## ブロックのISeqを書き換える

この辺のうちflagsはそれぞれの種類の引数があるかどうかをあらわしている。

```
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param
$70 = {flags = {has_lead = 0, has_opt = 0, has_rest = 0, has_post = 0, has_kw = 0, has_kwrest = 0,
    has_block = 0, ambiguous_param0 = 0}, size = 0, lead_num = 0, opt_num = 0, rest_start = 0,
  post_start = 0, post_num = 0, block_start = 0, opt_table = 0x0, keyword = 0x0}
```

`foo {|x| x }`のような、１つ引数を取るブロックの場合は以下のようになっている。

```
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param
$71 = {flags = {has_lead = 1, has_opt = 0, has_rest = 0, has_post = 0, has_kw = 0, has_kwrest = 0,
    has_block = 0, ambiguous_param0 = 1}, size = 1, lead_num = 1, opt_num = 0, rest_start = 0,
  post_start = 0, post_num = 0, block_start = 0, opt_table = 0x0, keyword = 0x0}
```

それに合わせて`flags`の`has_lead`と`size`と`ambiguous_param0`を1に書き換えていく。

```
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param.flags.ambiguous_param0 = 1
$75 = 1
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param.flags.has_lead = 1
$76 = 1
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param.size = 1
$77 = 1
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.param
$78 = {flags = {has_lead = 1, has_opt = 0, has_rest = 0, has_post = 0, has_kw = 0, has_kwrest = 0,
    has_block = 0, ambiguous_param0 = 1}, size = 1, lead_num = 0, opt_num = 0, rest_start = 0,
  post_start = 0, post_num = 0, block_start = 0, opt_table = 0x0, keyword = 0x0}
```

これでスタックに値が積まれているはず、continueしてみる。

```
(gdb) c
Continuing.
-e:1: [BUG] Stack consistency error (sp: 14, bp: 13)
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]

-- Control frame information -----------------------------------------------
c:0004 p:0004 s:0014 e:000013 BLOCK  -e:1
c:0003 p:0004 s:0010 e:000009 METHOD -e:1
c:0002 p:0029 s:0006 E:002680 EVAL   -e:1 [FINISH]
c:0001 p:0000 s:0003 E:000460 (none) [FINISH]
```

エラーが出る。`git grep`でエラーメッセージを調べると以下がヒット。

```vm_insnhelper.c
static void
vm_stack_consistency_error(const rb_execution_context_t *ec,
			   const rb_control_frame_t *cfp,
			   const VALUE *bp)
{
    const ptrdiff_t nsp = VM_SP_CNT(ec, cfp->sp);
    const ptrdiff_t nbp = VM_SP_CNT(ec, bp);
    static const char stack_consistency_error[] =
	"Stack consistency error (sp: %"PRIdPTRDIFF", bp: %"PRIdPTRDIFF")";
#if defined RUBY_DEVEL
    VALUE mesg = rb_sprintf(stack_consistency_error, nsp, nbp);
    rb_str_cat_cstr(mesg, "\n");
    rb_str_append(mesg, rb_iseq_disasm(cfp->iseq));
    rb_exc_fatal(rb_exc_new3(rb_eFatal, mesg));
#else
    rb_bug(stack_consistency_error, nsp, nbp);
#endif
}
```
https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L3329-L3346

`vm_stack_consistency_error`で探すと`leave`する際にしかこのエラーは出ないよう。

```c:insns.def
/**
  @c method/iterator
  @e return from this scope.
  @j このスコープから抜ける。
 */
DEFINE_INSN
leave
()
(VALUE val)
(VALUE val)
{
    if (OPT_CHECKED_RUN) {
	const VALUE *const bp = vm_base_ptr(reg_cfp);
	if (reg_cfp->sp != bp) {
	    vm_stack_consistency_error(ec, reg_cfp, bp);
	}
    }


    RUBY_VM_CHECK_INTS(ec);


    if (vm_pop_frame(ec, GET_CFP(), GET_EP())) {
#if OPT_CALL_THREADED_CODE
	rb_ec_thread_ptr(ec)->retval = val;
	return 0;
#else
	return val;
#endif
    }
    else {
	RESTORE_REGS();
    }
}
```
https://github.com/ruby/ruby/blob/v2_5_1/insns.def#L961-L992

`vm_base_ptr`の計算を見てみる

```vm_insnhelper.c
static VALUE *
vm_base_ptr(const rb_control_frame_t *cfp)
{
    const rb_control_frame_t *prev_cfp = RUBY_VM_PREVIOUS_CONTROL_FRAME(cfp);


    if (cfp->iseq && VM_FRAME_RUBYFRAME_P(cfp)) {
	VALUE *bp = prev_cfp->sp + cfp->iseq->body->local_table_size + VM_ENV_DATA_SIZE;
	if (cfp->iseq->body->type == ISEQ_TYPE_METHOD) {
	    /* adjust `self' */
	    bp += 1;
	}
#if VM_DEBUG_BP_CHECK
	if (bp != cfp->bp_check) {
	    fprintf(stderr, "bp_check: %ld, bp: %ld\n",
		    (long)(cfp->bp_check - GET_EC()->vm_stack),
		    (long)(bp - GET_EC()->vm_stack));
	    rb_bug("vm_base_ptr: unreachable");
	}
#endif
	return bp;
    }
    else {
	return NULL;
    }
}
```
https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L1522-L1546

`local_table_size`の書き換えが必要。

もう一度同じところまで進めてlocal_table_sizeも合わせて書き換える

```
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.ise
q->body.local_table_size
$82 = 0
(gdb) p VM_BH_TO_ISEQ_BLOCK(VM_CF_BLOCK_HANDLER((ruby_current_execution_context_ptr->cfp + 2)))->code.iseq->body.local_table_size = 1
(gdb) c
Continuing.
[Thread 0x7ffff7ff6700 (LWP 20326) exited]
[Inferior 1 (process 20325) exited normally]
```

正常終了するようになった

## 積まれた引数を取得する

ブロックを書き換えて、CFPに`yield`に渡された値が捨てられずにスタックに積まれたままになった。

`vm_invoke_iseq_block`を呼び出したcfpでは`SET_SP(rsp);`でspが`yield`で積んだとこを指してるので、`vm_invoke_block`を呼び出したcfpのspにアクセスすると取れる。

### スタックの様子

ブロック呼び出し時のスタックの様子

```vm_insnhelper.c
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);
```

この時点ではこういう感じ、`cfp->sp`が引数の1つ上を指してる。引数の個数が1つなんでrspはspの1つ下

![_home_sei_src_github.com_hanachin_stackgraph_index.html.png](https://qiita-image-store.s3.amazonaws.com/0/2074/7a9bb2c3-c33f-c9ac-1b65-40770ac2c371.png)


```vm_insnhelper.c

    SET_SP(rsp);
```

`SET_SP`呼び出すと`cfp->sp`がここに移動する

![_home_sei_src_github.com_hanachin_stackgraph_index.html (2).png](https://qiita-image-store.s3.amazonaws.com/0/2074/ed8a8742-9de3-2af4-c31b-723040dd61ac.png)

vm_push_frame呼び出す

```vm_insnhelper.c

    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;
}
```
https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L2623-L2646

`vm_push_frame`では新しいcfpが作られる

```vm_insnhelper.c
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;
}
```
https://github.com/ruby/ruby/blob/v2_5_1/vm_insnhelper.c#L199-L255

このときISeqのブロックの引数が0の時と引数1のときでは`vm_push_frame`に渡されるspの位置がこういう感じでずれている。

![_home_sei_src_github.com_hanachin_stackgraph_index.html (3).png](https://qiita-image-store.s3.amazonaws.com/0/2074/9f0c32c6-69d6-e7d3-2f67-31443d4a8ab0.png)

ISeqの引数0のときはこういう感じでspに積まれていた42が上書きされてしまう。

![_home_sei_src_github.com_hanachin_stackgraph_index.html (4).png](https://qiita-image-store.s3.amazonaws.com/0/2074/82728fb7-67b7-07ea-3426-aa13c0d48285.png)

ISeqの引数が1のときはこういう感じで上書きされず残っている

![_home_sei_src_github.com_hanachin_stackgraph_index.html (5).png](https://qiita-image-store.s3.amazonaws.com/0/2074/e0c757f1-985b-450b-7df3-63bd9c555edf.png)

### 積まれた引数を取るメソッドを生やす

こういうプログラムだと

```rb
def foo
  yield 42
end

foo { it }
```

`it`メソッド呼び出した際のcontrol frameのスタックはこうなっている

![_home_sei_src_github.com_hanachin_stackgraph_index.html (7).png](https://qiita-image-store.s3.amazonaws.com/0/2074/05851487-e822-d850-fd46-679383ce893a.png)


なので`it`で積まれた引数を取るには以下のような感じの2通りのやり方で取れる

```rb
def it
  # ここで1つ上のcfpを参照するとブロックのcfpが取れる、1つ上のcfp->ep - 3にアクセスする
  # or
  # ここで2つ上のcfpを参照するとブロックを呼び出したときのcfpが取れる、メソッド呼び出し時のcfp->spにアクセスする
end
```


![_home_sei_src_github.com_hanachin_stackgraph_index.html (6).png](https://qiita-image-store.s3.amazonaws.com/0/2074/6579e4c0-187e-7061-ca01-3123dbf0c48b.png)

## 実装

`rb_control_frame_t`の型などはrubyのAPIとしてinclude出来るヘッダーが公開されていないので必要な部分をコピーしてくる。

バージョンごとに構造体が違うのでバージョンごとの定義を用意する。
https://github.com/hanachin/thats_it/blob/master/ext/thats_it/thats_it_20500.h
https://github.com/hanachin/thats_it/blob/master/ext/thats_it/thats_it_20600.h

`RUBY_API_VERSION`で分岐して各バージョンにあった定義を読み込む。

```thats_it_20600.h
#if RUBY_API_VERSION_CODE >= 20600
#include "thats_it_20600.h"
#elif RUBY_API_VERSION_CODE >= 20500
#include "thats_it_20500.h"
#endif
```
https://github.com/hanachin/thats_it/blob/master/ext/thats_it/thats_it.h#L11-L15

現在実行中のcontrol frameはこんな感じでexternしておいて

```c
extern rb_execution_context_t *ruby_current_execution_context_ptr;
```

こうやれば取れる

```c
ruby_current_execution_context_ptr->cfp
```

control frameやiseq書き換え周りはCで書いている。
https://github.com/hanachin/thats_it/blob/master/ext/thats_it/thats_it.c

TracePoint仕掛けるのはrubyの方が楽に書けそうなのでrubyで書いた。
https://github.com/hanachin/thats_it/blob/master/lib/thats_it.rb

## まとめ

C拡張を書けばTracePointでメソッド呼び出し時に強引にブロックのISeqを取得できる。
メソッド呼び出し時のタイミングであれば渡されたブロックのISeqを書き換えられる。
ブロックは呼び出されるまで評価されないためISeqを実行する前にISeqを書き換えられる。

マクロだーーー!!!

完