はじめに
WebAssemblyにはbr
という命令があります。
この命令はbr <label>
という形になっており、<label>
が示す制御構造に対して何らかの動作をします。
例えばブロックに対して使えばブロックを抜け、ループの中で使えばcontinue
のような振る舞いをします。
wasmのループについてはκeenさんのWebAssemblyのloopはまりどころという記事を見てみて下さい。
なぜこのように制御構造によってブロックを抜ける働きをしたり、continue
の働きをしたりするか仕様書を読んでいたところ理由が分かり面白いと思ったので記事にしました。
br命令
基本的にはbr 数値
という形の命令です。この数値は相対的な値となっておりbr
命令を囲っているすぐ外側の制御構造に対してなにかするときはbr 0
と、その外側であればbr 1
と...いうふうに指定します。
また、watでは制御構造に$label
をつけることでbr $label
と書けばbr
命令がどの深さに関係なくその制御構造に対して命令を実行することができます。
例
関数に対して使うとその関数を抜けます。
(call $log (i32.const 1))
(br 0)
(call $log (i32.const 2))
;; output: 1
ブロックで使うとそのブロックを抜けます。
(block $label
(call $log (i32.const 1))
(br $label)
(call $log (i32.const 2))
)
(call $log (i32.const 3))
;; output: 1 3
ちなみにこのwatはwasmにコンパイルすると以下のコードと等しくなります。
(block
(call $log (i32.const 1))
(br 0)
(call $log (i32.const 2))
)
(call $log (i32.const 3))
以下のコードはbr 1
としているのでbr
命令の2つ外側の制御構造、つまり関数を抜けます。
(block
(call $log (i32.const 1))
(br 1)
(call $log (i32.const 2))
)
(call $log (i32.const 3))
;; output: 1
ループに対してはcontinue
として働きます。br
がなければループは一回しか実行されません。
br_if
は引数が0
でなければbr
を実行する命令です。
(local $n i32)
(loop $loop
(call $log (get_local $n))
(set_local $n (i32.add (get_local $n) (i32.const 1)))
(br_if $loop (i32.ne (get_local $n) (i32.const 3)))
)
;; output: 0 1 2
labelとbr命令の正体
なぜbr
は対象の制御構造によってブロックを抜けたりcontinue
として働いたりするのでしょうか。
これはbr n
はn番目に外側(すぐ外側は0番目)のlabel
命令の継続にジャンプする命令だからです。
label
命令というのは仕様を書くために出てくる拡張命令みたいなものでwasmコード自体には現れません。
label
命令はlabel {継続} {命令}
のような形になっています。そして通常は{命令}
を評価しそれが終われば、label
の評価は終了します。br
されれば{命令}
の評価を中断し、{継続}
を評価し、それが終わればlabel
の評価は終了します。
そしてblock
やloop
を評価すると一旦このlabel
命令に変換されます(これは仕様書上の話で実際の処理系がこうなっているという事ではない)。また関数に入ったときもlabel
が作られます(これによって関数の直下でbr 0
とすると関数を抜けられます。)
例えばblock
であればlabel {} {...}
のように変換されるのでbr
でジャンプする継続は空です。つまりbr
されれば何もせずにブロックを抜けます。
if
や関数に入った時も継続が空のlabel
に変換されるのでbr
されれば何もせずにその制御構造を抜けます。
しかしloop
はlabel {loop ... end} {...}
のように変換されます。もしloop
に対してbr
すればlabel
の継続であるloop ... end
にジャンプし、これが評価されてまたlabel
に変換されlabel {loop ... end} {...}
となり…を繰り返す事でループが実現しています。これがloop
に対するbr
がcontinue
として働く理由です。またbr
しなければそのままlabel
を抜けることになるのでこれによってloop
に対してbr
しなければ一回しか処理は実行されません。
例
先ほど例として出した以下のループの実行を例にします。(label命令は仕様書の中だけに出てくるものでwatには存在しないのでコンパイルはできません)
(local $n i32)
(loop $loop
(call $log (get_local $n))
(set_local $n (i32.add (get_local $n) (i32.const 1)))
(br_if $loop (i32.ne (get_local $n) (i32.const 3)))
)
;; output: 0 1 2
loop
を評価するとlabel
に変換します。
;; n: 0
(loop $loop ;; ←評価
(call $log (get_local $n))
(set_local $n (i32.add (get_local $n) (i32.const 1)))
(br_if $loop (i32.ne (get_local $n) (i32.const 3)))
)
(label $loop
{loop $loop
(call $log (get_local $n))
(set_local $n (i32.add (get_local $n) (i32.const 1)))
(br_if $loop (i32.ne (get_local $n) (i32.const 3)))
}
{
(call $log (get_local $n))
(set_local $n (i32.add (get_local $n) (i32.const 1)))
(br_if $loop (i32.ne (get_local $n) (i32.const 3)))
}
)
label
では継続ではなく本体を順に評価します。
評価3
を実行するとbr_if
の引数は1
なのでbr
を実行します。
この時継続にジャンプし、継続を評価します。ここでの継続はloop
です。
これによってloop
が評価されlabel
になり…を繰り返します。
;; n: 0→1
(label $loop
{loop $loop
(call $log (get_local $n))
(set_local $n (i32.add (get_local $n) (i32.const 1)))
(br_if $loop (i32.ne (get_local $n) (i32.const 3)))
}
{
(call $log (get_local $n)) ;; ←評価1
(set_local $n (i32.add (get_local $n) (i32.const 1))) ;; ←評価2
(br_if $loop (i32.ne (get_local $n) (i32.const 3))) ;; ←評価3
}
)
;; n: 1
(loop $loop
(call $log (get_local $n))
(set_local $n (i32.add (get_local $n) (i32.const 1)))
(br_if $loop (i32.ne (get_local $n) (i32.const 3)))
)
n
が3
の時br_if
の引数は0
なので何もしません。つまり継続には飛ばずそのままlabel
の本体の処理が終わります。つまりループが終了します。