Open16

Shiika ブロック内return 実装

returnの飛び先はレキシカルに決まるんだけど、判定はダイナミックにやらないといけない

  1. @user_main
  2. @A.return_from_block
  3. @Array#map
  4. @Array#each (mapはeachを使って実装されているため)
  5. @lambda_i
  6. @Array#each
  7. @lambda_j

という順で6種7個のllvm functionが呼ばれたあと、7,6,5,4,3,2と処理が戻っていって、2のところで「内部のブロックでreturn 99されたので99を返して終了」としたい

例外の実装みたいなことをすれば実装可能、それはそう
しかしShiikaにはまだ例外システムがない(いれるかどうかも未定)

メソッド名を使う案(仮)

  • 「このメソッドまで戻る」がやりたいわけなので、戻りたいメソッド名をpropagateする
  • cons: 実行時に「現在のメソッド名」を持つ必要がある
  • cons: 同じメソッドの呼び出しが2個あるケースがないだろうか
  def foo -> Int
    method1 do
      return 123
    end
    456
  end

このreturnはfooを抜けてほしい。
問題はmethod1の中で何が行われるか、ここだけでは分からないということ
例えばmapの場合はもらったブロックをeachにそのまま渡している

  def method1(f: Fn0<Int>)
    ??
  end

スタックをどこまで巻き戻せばよいか。

  def foo -> Int
    ...
    a { b { c { ... z { return 1 } ... }
    ...
  end

この場合、aの呼び出しが終端。
一般に、ブロック内returnを見つけたら、そこから上にたどっていって、メソッドトップレベルでのメソッド呼び出しが「終端」になる?

cのところとかで止まってしまうケースがないか考えてた。
上記を実現するためにはb〜yは「内側のメソッド呼び出しがreturn in blockで終了した」という情報をforwardする必要がある。
が、たとえば以下を考えると

  def c(block: Fn0<Void>) -> Int
    d {    
      block()
      return 1
    }
  end
  1. dの呼び出しは「終端」の条件を満たしている
  2. 一方、前項のaからreturnまでの間にこのdの呼び出しを通る
  3. 1として考えると、dの呼び出し後は「これがcの返り値です」として1を返したい
  4. 2として考えると、dの呼び出し後は「内側のメソッド呼び出しがreturn in blockで終了しました」としてnullを返したい

ので1, 2を区別する必要がある。

より複雑?な例

  def c(block: Fn0<Void>) -> Int
    d {    
      block(){ return 2 }
      return 1
    }
  end

ので1, 2を区別する必要がある。

しかしdがブロックを実行したとき、dから見るとどちらの場合も「ブロックがreturn in blockで終了した」になってしまうので区別できない?

困ったときのcrystal。

def return_in_block
  [1,2,3].each do |i|
    return 99
  end
  -1
end
p return_in_block  #=> 99

.llを見る。あ、このブロックはインライン化されちゃうのか。
うーん、インライン化を避けるにはどうしたらいいんだろ。

def foo(&block)
  block.call
end

def return_in_block
  ret = [99]
  foo do
    return ret
  end
  ret
end
p return_in_block
In return-in-block.cr:8:5

 8 | return ret
     ^
Error: can't return from captured block, use next

!?

しかしdがブロックを実行したとき、dから見るとどちらの場合も「ブロックがreturn in blockで終了した」になってしまうので区別できない?

ブロックには固有の番号が振ってあるから、「何番のブロックがreturnを実行したか」を伝播すれば区別できる?

別の話として、ブロックを保存した場合は?というのがある。

class Watcher
  def on_changed(id: String, f: Fn1<String, Void>)
    @callback = f
  end
  def start
    loop do
      ...
      @callback(data)
    end
  end
  def self.init
    w = Watcher.new
    w.on_changed(id) do |data|
      .. return  # これが実行される頃には既にinitを抜けている。
    end
  end
end

Rubyだと確かreturnするタイミングでLocalJumpErrorだが、静的言語でこれがrun-time errorになるのは最悪なので、compile-time errorにしなければならない。
…が、できそうな気がしないなあ。

Rubyにlambdaとprocがあるように、関数とブロックでクラスを分けるしかないのかも。
名前をどうするか。FnとProcかなあ。

  • どちらも専用の構文があり、.newで作ることはできない。
  • FnからProcへの変換はできる。
  • ProcからFnへの変換はできない(return/breakを使っていなければできる?)
    なんかあんまり筋がいい気がしないな。
ログインするとコメントできます