WebAssemblyをRubyにコンパイルするツールを作っている。
今日はWebAssemblyのif,block,loopとbr命令について考える。
if
(if 条件
(then
処理1
)
(else
処理2
)
)
これをRubyに移植する。
if 条件
処理1
else
処理2
end
と簡単にはいかない。ifの中でbr命令が出てくると、if節の最後までジャンプすることになる。
(if 条件
(then
br 0
処理1
)
(else
処理2
)
)
この場合はthenに入ったら処理1は実行されずにif節を抜ける。
この挙動はRubyにそのまま移植することはできない。こういうときのお決まりの方法はこれ。
catch(:foo) do
if 条件
throw :foo
処理1
else
処理2
end
end
次にblockとloopを考える。Rubyだとどちらもwhileで書ける。
block
block節もその中にbrがあるとループの最後にジャンプする。↓この例だと処理1は実行されるが処理2は実行されない。
(block
処理1
br 0
処理2
)
while true
処理1
break
処理2
break # brを呼ばなかったとしてもここで必ず抜ける
end
loop
逆にloopはその中でbrがあると、節の頭にジャンプする。↓この例だと処理1が無限に実行され続ける。
(loop
処理1
br 0
処理2
)
while true
処理1
next
処理2
break # brを呼ばなかったらここで必ず抜ける
end
多段
block→if
(block
(if 条件
(then
br 1 ;; 0だとif節から抜けるが、1だとblock節から抜ける
)
(else
)
)
)
while true
catch(:foo) do
if 条件
break # 0だとthrowだが、1だとbreakになる
else
end
end
break # 必ず抜ける
end
block→block
(block
(block
br 1 ;; 外側のblockを抜ける
)
)
Rubyではラベルつきbreakが無いので、blockでもcatchを使ったほうがいい、ということになる。
catch(:foo) do
while true
catch(:bar) do
while true
throw :foo # 0だとbarだが、1だとfooになる
break # 必ず抜ける
end
end
break # 必ず抜ける
end
end
loop→block
(loop
(block
br 1 ;; 外側のloopを繰り返す
)
)
これだとcatchでもダメで、nextするための変数を導入するなどしなければならない。
while true
bar = catch(:bar) do
while true
throw :bar, :again # throwの第二引数はcatchの返り値になる
break # 必ず抜ける
end
end
next if bar == :again
break # 必ず抜ける
end
めんどくさい
if,block,loopによって、ネストの仕方によってbrの扱いを変えなければいけないのは面倒すぎる!
と思って考えた結果、メソッドに切り出すことにした。
# 第二引数はprocで第三引数はblock
def _if(condition, then_proc, &else_block)
if condition
then_proc.call
elsif else_block
yield else_block
else
-1
end
end
# 自分を指しているbrのみnextにする
def _loop(&block)
while true
depth = yield block
next if depth == 0
return depth
end
end
# 実行するだけ
def _block(&block)
yield block
end
これだと、何が良いかというと、
;; 1からnumまで足した結果を返す
(module
(func (export "sum") (param $num i32) (result i32)
(local $sum i32) ;; int sum
(local $i i32) ;; int i
(set_local $sum (i32.const 0)) ;; sum = 0
(set_local $i (i32.const 1)) ;; i = 1
;; while(true)
(block $block (loop $loop
(br_if $block (i32.gt_u (get_local $i) (get_local $num))) ;; if (i >= num) break
;; inside loop
(set_local $sum (i32.add (get_local $sum) (get_local $i))) ;; sum = sum + i
(set_local $i (i32.add (get_local $i) (i32.const 1))) ;; i = i + 1
(br $loop) ;; continue
))
(get_local $sum)
)
)
↑これが、↓こうなる。
;; 1からnumまで足した結果を返す
def sum(num)
sum = 0
i = 1
depth = _block{
depth = _loop{
next 1 if i > num # brではなくbr_if命令
sum = sum + i
i = i + 1
next 0
-1
}
next depth - 1 if depth > 0
-1
}
sum
end
- ifでもblockでもloopでも必ず
depth =
で始めれば良い - brは
next 深さ
に変換する - 正常に抜けるときは -1 を返す
- どれだけ多段ネストしてもブロックの終わりに
next depth - 1 if depth > 0
をつければよい(トップレベルだけは省略する)
if,block,loopの違いをまったく意識しなくても良くなった。
パフォーマンスは計測してない。