要注意な点
個々の 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のコンパイル時に準備