ひとり勉強会

ひとり楽しく勉強会

要注意な点

個々の break 文などのコンパイルは、いつも通り case NODE_BREAK で行われます。でも、その前に NODE_WHILE で while をコンパイルする時点で、あらかじめ準備しておかなくてはいけないことがいくつもあります。注意点を列挙してみました。

break, next, redo のジャンプ先管理

while 条件
  ... (ここらへん) ...
end

の (ここらへん) に break が出てきた場合、その break は「while ループの終わり」にジャンプするジャンプ命令へとコンパイルされます。NODE_WHILE のコンパイル中に再帰的にループ本体をコンパイルするときは、「今break文が出てきたらどこへのジャンプ命令を生成すればいいのか」要するに「今どこがwhile文の終わりなのか」を覚えながら再帰する必要があります。

while 条件1
  ...
  while 条件2
     ... break ... # A
  end
  ... break ... # B
end

while はネストするかもしれません。Aのジャンプ先とBのジャンプ先がごっちゃにならないように管理しないと大変です。

ensure と break, next, redo

Rubyには、ensure節という構文があります。「ensureのついたコードブロックをどんな方法で脱出したときでも、必ずensureの中身を実行する」という構文です。

while 条件
  begin
    ... break ... # C
  ensure
    (かならず実行)
  end
end

ensureを実現するには、Cのbreak文は単純にwhileの最後へのジャンプ命令にはできません。「ensureの中身を実行してから、whileの最後へジャンプ」みたいにコンパイルします。

begin
  while 条件
    ... break ... # D
  end
ensure
  (かならず実行)
end

でも、ensureの中にあるbreakだからといって、いつも「ensureの中身を動かしてからジャンプ」にコンパイルすると、上の例のときに失敗します。whileとensureの入れ子関係にも注意しないといけません。

ブロックと break, next, redo

while 条件
  some_object.each {|v|
    break # E
  }
end

Rubyには、ブロックという構文もあります。ブロックは「引数をとって処理を実行する」オブジェクトで、その意味ではメソッドに似ています。が、上の例の each のように、制御構文のように見えて制御構文のように書けるのが特徴です。
そして、本当に制御構文のように使えるのが重要です。# の break は、ちゃんとソースコード上で周囲にあるeachメソッドを抜けるbreakとして働きます。これはメソッドにはないブロックの特徴です。

def each_fun(f)
  f
end

def breaker
  break # こういうことはできない
end

each_fun(breaker)

とはいえ、ブロックの実態はメソッドに近くて、どこからどんな状態で呼び出されるかは不確かです。なので、ブロックの中のbreakも、単純なジャンプ命令には変換できません。そこでYARVでは、例外の仕組みを使ってブロックからのbreakを実現するようです。イメージとしては(正確ではありませんが)、さっきの例はこんな感じのコードと思ってコンパイルされます。

while 条件
  some_object.each {|v| raise BreakException.new }
    # each の中の yield の実行時に
    # break例外をうまく処理する
end

というわけで、breakをコンパイルするときには、ジャンプ命令に変換すればいいのか、例外に変換すればいいのか、場合によって分かれます。

break while 例外

while 条件
  class X
    break # F
  end

  begin
    ...
  rescue
    break # G
  end
end

例外にコンパイルされるのは、ブロックからのbreakだけではないみたいです。whileループからの脱出も、場合によっては例外になります。(むしろ、ジャンプ命令になる場合は最適化の結果、と思えばいいのかも。)
YARVでは、変数のスコープが独立しているという単位 (など) で命令列オブジェクトを分けています(YARV Maniacs 第8回)。
上の例のFやGのように、命令列オブジェクトの境界を越えるbreakは、例外で処理されるみたいでした。(推測ですが、別の命令列ブロックになっているとラベルとかの互換性がなくて、単純なジャンプだけでは済まないという理由なのではないかと思います。違うかも。)
この場合を処理するためには、whileのコンパイルの時点で、break例外を捕まえる処理を準備しておくことになります。イメージとしては下のような雰囲気。

begin
  while 条件
    class X
      raise BreakException.new
    end
  end
rescue BreakException # 例外でbreakされた時の対処
end                   # whileのコンパイル時に準備

break 値

x = while true
  break 123
end

break に値を渡すと、while式全体の式の値になります。CやJavaのbreakとRubyで違うところですね。ところで、while式全体の式の値が必要かどうかは、前々回 やったようにpopedというフラグで渡されます。値が必要ない場合はbreakを最適化できるように、whileの時点でのpopedフラグも、breakのために覚えておくようになっています。

長かった。。。ここからは、このへんそれぞれどう実装されてるか読んできます。