VM命令:ジャンプ
LuaVMには純粋なジャンプ命令は1つしかありません。相対距離をオペランドにとって、pcを変化させる無条件ジャンプです。
case OP_JMP: { dojump(L, pc, GETARG_sBx(i)); continue; }
dojumpマクロの定義はこうで
#define dojump(L,pc,i) {(pc) += (i); luai_threadyield(L);}
スレッドの話を忘れることにすると、本当に単にpcにジャンプ距離を足し算しているだけです。(そういえば、割り込み的な処理をジャンプ命令のタイミングでやるのはYARVでもやってましたね。閑話休題)
では条件分岐はどうするのでしょう・・・?これは、比較演算などの条件判定式と無条件ジャンプの組み合わせで実現されています。例として < 演算子の実装命令は
case OP_LT: { Protect( if (luaV_lessthan(L, RKB(i), RKC(i)) == GETARG_A(i)) dojump(L, pc, GETARG_sBx(*pc)); ) pc++; continue; }
こんなことになっています。A が 0 のとき RKB < RKC の判定で、A が 1 のとき !(RKB < RKC) を判定します。メタテーブルの検索順の関係で、後者を RKC <= RKB とは書けないのでAで場合分けしているのかなと思います。それはともかく、OP_LT は、比較が成立したら、次の命令(*pc)がJMP命令であることを前提にして、それをdojumpで実行してしまってます!(ちょっとややこしいですが、命令を読み取るとすぐにpc++するので、命令の実行時点ではpcは次の命令を指しています。JMP命令のsBxにはJMP命令の次の命令からの相対距離が入るようになっているので、dojumpしたあとさらにpc++すると正しい位置に飛びます。)比較が成立しなかった場合は、1回 pc++ することで、次のJMP命令をスキップしています。
つまり、LT→JMP のように条件判定演算と無条件ジャンプを必ず並べて使うことで、条件分岐が表現されているわけです。たとえば
if 1 < 2 then print("aaa") end print("bbb")
これをコンパイルすると
1 [1] LT 0 -1 -2 ; 1 2 1 < 2 でなければ 2 [1] JMP 3 ; to 6 6へジャンプ!! 3 [2] GETGLOBAL 0 -3 ; print 4 [2] LOADK 1 -4 ; "aaa" 5 [2] CALL 0 2 1 6 [4] GETGLOBAL 0 -3 ; print 7 [4] LOADK 1 -5 ; "bbb" 8 [4] CALL 0 2 1 9 [4] RETURN 0 1
こうなります。このようにJMPと組み合わせることが前提の命令はいくつかあります。
- 比較演算子 OP_EQ, OP_LT, OP_LE (if a < b then ... のような時に使う)
- bool変数の評価 OP_TEST, OP_TESTSET (if flag then .. のような時に)
- bool定数 OP_LOADBOOL
他にforループ用の命令でも使われていますがこれは後ほど。ところで、比較演算子は必ずJMPと組み合わせる用に定義されているので、if文の条件部以外に比較演算が来る場合、ちょっと面白いことになります。例をどうぞ。
local a = 1 < 2
これをコンパイルするとこうなります。
1 [1] LT 1 -1 -2 ; 1 2 1<2 だったら 2 [1] JMP 1 ; to 4 4にジャンプ 3 [1] LOADBOOL 0 0 1 R[0] = 0 && 次の命令をスキップ 4 [1] LOADBOOL 0 1 0 R[0] = 1 5 [1] RETURN 0 1
要するに
if 1 < 2 then a = true else a = false end
のような分岐っぽいコードになるわけですが、このケースの最適化のためにLOADBOOLという専用命令があります。
case OP_LOADBOOL: { setbvalue(ra, GETARG_B(i)); if (GETARG_C(i)) pc++; /* skip next instruction (if C) */ continue; }
「R[A]レジスタにBの値をboolとしてセットしつつ、Cが1なら次の命令をスキップ」しています。