Inside yarv2llvm(その6)

イテレータ(ブロック付メソッド呼び出しの方が正確なのかな?)の説明です。yarv2llvmではイテレータに渡すブロックはクロージャーとして実現しています。言い換えると、イテレータは無名のクロージャーを引数に渡すメソッドであるといえます。例えば、

class Fixnum
  def times
    i = 0
    while i < self 
      yield(i)    # 1
      i = i + 1
    end
    self
  end
end

i = 1
10.times do |n|  # 2
  p n + i
  i = i + 1
end
p i

とすると、イテレータに渡すブロック

do |n|
  p n + i
  i = i + 1
end

の部分が、あたかもメソッドのようにコンパイルされ、そのポインタがtimesに渡されます。そうすると、iの参照ってどうするの?という疑問が生じるかもしれません。ここで注意すべきは、ブロックを呼び出すのは字面から# 2のところと考えてしまうかもしれませんが(私だけ?)、実際にはブロックの中身の情報を全然知らない(複数の場所からtimesが呼ばれた場合もありえるし)timesの中(#1)であるということです。こういうことから、例えば、ブロックの外側にある変数を隠し引数として渡すという方法は使えません。
yarv2llvmでは、隠し引数として(その5)で説明したローカル変数の構造体を渡すようにしてブロックの外側の変数のアクセスを実現しています。ここで、ローカル変数の構造体を渡すのは、ブロックだけではなく、イテレータ(ここでは、times)にも渡します。イテレータはyieldでブロックを起動するとき、この構造体も渡すようにコンパイルされます。
結局、イテレータには普通の引数に加えて二つの隠れ引数(イテレータを呼び出したローカル変数の構造体とブロックへのポインタ)を渡します。ここで、イテレータじゃない普通のメソッドではこの引数を渡す必要がないので渡しません。イテレータかそうじゃないかの違いはyieldがあるか無いかで判断します*1

それでは、渡された引数の構造体を使ってどうやって変数をアクセスしているか説明します。実は、ここからは私は何もしていなくて、ただYARVllvmの優れた機能をそのまま使っているだけです。それでは、その優れた機能を説明します。

YARVではブロック内の変数のアクセスは getdynamic命令を使います。getdynamic命令は変数のオフセットとブロックのネストの深さを引数に取ります。
LLVM*2では構造体を0番目のメンバーのアドレス、1番目のアドレスという感じで数でアクセスするstruct_gepという命令があります。これを使うとgetdynamic命令が想定する順番に構造体のメンバー(ローカル変数)を並べておけば何も考えずに変数にアクセスできます。そして、ローカル変数の順番はRubyVM::InstructionSequence.compile(...).to_aで返って来た配列のヘッダ部分に書いてあります。このヘッダの内容は多分どこにも情報は無いので、詳しくはRubyのソース(compile.c, iseq.c)を読んでください。
ブロックの中にブロックがある場合どうなるの?って思われるかもしれませんが、ここで活躍するのがネストの深さです。Ruby1.9の処理系がコンパイルするときどれだけ外側のブロックの変数かを調べて記録しておいてくれています。何も考えずにその数だけ外側に辿っていけば、ブロックが重なっても大丈夫です。外側に辿るってどうやって?と思われるかもしれませんが、ブロックの外側のローカル変数の構造体は隠れ引数として渡ってくることを思い出しましょう。判りづらいかもしれませんが、ブロックの外側の外側の構造体はブロックの外側の構造体のメンバーに含まれています。同様に、ブロックの外側の外側の外側の構造体はブロックの外側の外側の構造体のメンバーにふくまれていて・・・、ややこしいのでやめますが、Cで書くとこんな感じでです。

struct local_var1 {
  struct local_var0 *outer_local;  /* 外側のローカル変数へのポインタ */
  void *block;                     /* yaildで実行するブロック */
  int i;                           /* ローカル変数 */
}

実際にはキャストしまくりの汚いコードが出てきます。

判りづらい説明になってしまいました。すみません。
次はどうしようかなー、そろそろ新しいことをやろうかなと思います。

とりあえず、中締め

*1:実際には&引数を使ってブロックをProcオブジェクトとして取り出してcallすると、yaildの無いイテレータができますが、これを実現するとローカル変数をスタックに取れなくなるのでまだ実現していません。Rubyのyaildで起動されるブロックは明示的に宣言しなければfirst classじゃ無いという仕様は優れた設計だと思います。それは、エスケープ解析しなくても変数をヒープに取るかスタックに取るかを決められるからです

*2:llvmrubyの機能のような気もする