Shiika ブロック内return 実装
returnの飛び先はレキシカルに決まるんだけど、判定はダイナミックにやらないといけない
- @user_main
- @A.return_from_block
- @Array#map
- @Array#each (mapはeachを使って実装されているため)
- @lambda_i
- @Array#each
- @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
- dの呼び出しは「終端」の条件を満たしている
- 一方、前項のaからreturnまでの間にこのdの呼び出しを通る
- 1として考えると、dの呼び出し後は「これがcの返り値です」として1を返したい
- 2として考えると、dの呼び出し後は「内側のメソッド呼び出しがreturn in blockで終了しました」としてnullを返したい
ので1, 2を区別する必要がある。
困ったときの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
!?
captured blockという概念があるのか。crystalはこのケースは諦めてるっぽいなあ。
しかし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を使っていなければできる?)
なんかあんまり筋がいい気がしないな。