WebAssemblyにはbrというジャンプ命令が存在するが、
これはかなり癖がある。正直使いにくい。
常識的なジャンプ命令でできることができない。
この使いにくいジャンプ命令を使ってC言語のgoto相当のことをやる方法を考える。
WebAssemblyにはifという命令があり、
高級言語の条件分岐をそのまま翻訳しやすいようになっている。
(if (condition)
(then
(instruction1)
...
(instructionN))
(else
(instruction1)
...
(instructionM)))
このコードはまず(condition)を実行し、
その結果が1ならthenの内部を、0ならelseの内部を実行する。
これはこれで便利なのだが、好きなところにジャンプするような
低レベルのジャンプを実現するのには適していない。
ループを実現するにはloopという命令を使う。
loop命令自身はラベルを定義するために使い、
実際のジャンプは br (無条件ジャンプ) や br_if (条件付きジャンプ) を使う。
(loop $loop1
...
(br_if $loop1 condition))
このコードではloop内部のコードが上から実行され、
もしconditionが1になればbr_ifによりジャンプが行われ、
loopの先頭に戻り、再度実行を行う。
brやbr_ifは末尾に置く必要はなく好きな箇所に書ける。
brやbr_ifがジャンプを行わなければそのままloopを抜ける。
このloopはC言語で言えば末尾にbreakが置かれた無限ループ
while(1) { /* do something */; break; } に相当し、
brやbr_ifはcontinueに相当する。
loopを入れ子にできることを考えると
Javaなどのラベル付きcontinueと言ったほうが正確かもしれない。
C言語のcontinueに相当する命令もあれば、breakに相当する命令もある。blockだ。
WebAssemblyのblockはloopとほぼ同じ見た目をしているが、
brやbr_ifがジャンプを行う際には先頭に戻るのではなく、blockから抜ける。
(block $block1
...
(br_if $block1 condition)
...
)
このコードではblock内部のコードが上から実行され、
もしconditionが1になればbr_ifによりジャンプが行われ、
blockから抜ける。つまりbr_if以降のコードは実行されない。
blockを単独で使うのは、C言語でいえば do { ... } while(0); に相当し、
brやbr_ifはbreakに相当する。
blockとloopを組み合わせて (block $b (loop $l ...)) とした場合は、
(br $l)はcontinue、(br $b)はbreakに相当する。
もちろん入れ子にできるのでラベル付きbreakという方が正確だが。
ここから分かるようにbrやbr_ifといったジャンプ命令は、
指定されたラベルの種類によって動作が変化する。
正直やめてもらいたい。別の命令を使ってほしい。
それではこれらの命令を使ってgotoを実現する方法を考える。 まずは前向きのgotoの実現方法を見ていく。 次のようなC言語のプログラムを考える。
code1();
if (test1()) goto label1; // Forward jump
code2()
label1:
code3();
これをWebAssemblyで書くと次のようになる。
(block $block1
(call $code1)
(br_if $block1 (call $test1))
(call $code2))
(call $code3)
C言語のラベルより上の内容はblockの中に書き、
ラベルより下の内容はblockの外に書く。
ここは試験に出るので覚えておくように。
次はもう少し複雑な「ラベルを飛び越す」gotoを見ていく。
code1();
if (test1()) goto label1; // Forward jump
if (test2()) goto label2; // Forward super jump
code2()
label1:
code3();
label2:
code4();
上のgoto label2はlabel1を飛び越してlabel2にジャンプする。
C言語なら何も気にならないのだが、WebAssemblyでは工夫が必要になる。
(block $block2
(block $block1
(call $code1)
(br_if $block1 (call $test1))
(br_if $block2 (call $test2))
(call $code2)) ;; end of $block1
(call $code3)) ;; end of $block2
(code4)
blockが入れ子になった。$block2の方が外側にあるというのがポイントだ。
中身に関しては「ラベルより上の内容はblockの中に、
ラベルより下の内容はblockの外に」という原則をそのまま適用できる。
ラベルが2つ出てきただけでコードが複雑になり先行きが不安になってくる。
次は後ろ向きのgotoの実現方法を見ていく。
code1();
label1:
code2();
if (test1()) goto label1; // Backward jump
code3();
先程のC言語のプログラムのコードの順序を少し入れ替えたものだ。
これをWebAssemblyで書くとblockではなくloopを使うことになる。
(call $code1)
(loop $loop1
(call $code2)
(br_if $loop1 (call $test1))
(call $code3))
C言語のラベルより上の内容はloopの外(上)に書き、
ラベルより下の内容はloopの中に書く。
厳密には最後のgotoより下の内容はloopの外(下)に書くこともできるが、
ラベルより下の内容はすべてloopの中に書いた方が話が単純になる。
前向きgotoと同様に、次は「ラベルを飛び越す」gotoを見ていく。
code1();
label1:
code2();
label2:
code3();
if (test1()) goto label1; // Backward super jump
if (test2()) goto label2; // Backward jump
code4();
これをWebAssemblyで書くにはもちろんloopを入れ子にする。
(call $code1)
(loop $loop1
(call $code2)
(loop $loop2
(call $code3)
(br_if $loop1 (call $test1))
(br_if $loop2 (call $test2))
(call $code4)))
「ラベルより上の内容はloopの外(上)、ラベルより下の内容はloopの中」
という原則を守れば自然とこのコードが得られる。
比較的C言語のプログラムとの対応関係が取れているため、
前向きgotoの場合よりは読みやすいかもしれない。
道具は出揃ったので、前向きgotoと後ろ向きgotoの両方を含むプログラムを考える。
code1();
if (test1()) goto label1; // Forward jump
code2();
label1:
code3();
if (test2()) goto label1; // Backward jump
code4();
これをWebAssemblyで書くにはblockとloopの両方を使うことになる。
(block $block1
(call $code1)
(br_if $block1 (call $test1))
(call $code2)) ;; end $block1
(loop $loop1
(call $code3)
(br_if $loop1 (call $test2))
(call $code4))
一見複雑な見た目をしているが、
「ラベルより上の内容はblockの中、ラベルより下の内容はblockの外(下)」
「ラベルより上の内容はloopの外(上)、ラベルより下の内容はloopの中」
という2つの原則を守ると自然に書くことができる。原則が便利すぎる。
それでは最後に「ラベルを飛び越す」gotoを見ていく。
code1();
if (test1()) goto label2; // Forward super jump
code2();
label1:
code3();
label2:
code4();
if (test2()) goto label1; // Backward super jump
code5();
さて、これをWebAssemblyで書いてこの記事を終わらせたいところだが、 残念ながらこれをそのままWebAssemblyに翻訳することはできない。
前向きgotoに関して
「ラベルより上の内容はblockの中、ラベルより下の内容はblockの外(下)」
という原則を守ると次のようなプログラムが得られる。
(block $block2
(block $block1
(call $code1)
(br_if $block2 (call $test1))
(call $code2)) ;; end of $block1
(call $code3)) ;; end of $block2
(call $code4)
(br_if $loop1 (call $test2)) ;; TODO: Define $loop1
(call $code5)
一方、後ろ向きgotoに関して
「ラベルより上の内容はloopの外(上)、ラベルより下の内容はloopの中」
という原則を守ると次のようなプログラムが得られる。
(call $code1)
(br_if $block2 (call $test1)) ;; TODO: Define $block2
(call $code2)
(loop $loop1
(call $code3)
(loop $loop2
(call $code4)
(br_if $loop1 (call $test2))
(call $code5)))
この2つのプログラムを合体させたいのだが、
(call $code3) と (call $code4) に注目してほしい。
(call $code3)は「$block2の内側」かつ「$loop1の内側」に存在する。
(call $code4)は「$block2の外側」かつ「$loop1の内側」に存在する。
つまり、$block2と$loop1が「部分的に」重なっている。
S式ではこのような部分的な重なりは表現できないため、
このようなプログラムを書くことはできない。
ちくしょう何でS式なんて採用したんだ。
先程の例がうまく行かなかったのは、 前向きgotoと後ろ向きgotoのラベルが混ざっていたからだ。 そこで、プログラムから後ろ向きgotoを消し去る。 正確には、後ろ向きgotoのためのラベルを先頭に一箇所だけ定義して、 そこに戻ってから前向きgotoをすることにする。 ジャンプ先を覚えるために変数を使用する。 これをC言語で書くと次のようになる。
int jump = 0, next = 0;
do {
if (next) {
jump = next;
next = 0;
}
switch (jump) {
case 0:
code1();
if (test1()) {
next = 2; // Forward super jump
continue;
}
code2();
case 1:
code3();
case 2:
code4();
if (test2()) {
next = 1; // Backward super jump
continue;
}
code5();
}
} while (next);
あとはこれをWebAssemblyに翻訳するだけだ。
(loop $loop
(if (i32.gt_u (local.get $next) (i32.const 0))
(then
(local.set $jump (local.get $next))
(local.set $next (i32.const 0))))
(block $block2
(block $block1
(br_if $block1 (i32.eq (local.get $jump) (i32.const 1)))
(br_if $block2 (i32.eq (local.get $jump) (i32.const 2)))
(call $code1)
(if (call $test1)
(then ;; Forward super jump
(local.set $next (i32.const 2))
(br $loop)))
(call $code2)) ;; end of $block1
(call $code3)) ;; end of $block2
(call $code4)
(if (call $test2)
(then ;; Backward super jump
(local.set $next (i32.const 1))
(br $loop)))
(call $code5))
うーん、これはconsidered harmful。
2022-02-13