おまけ:LuLu (2)
というわけで、Lua VM on Lua 略して LuLu を実装するシリーズ第2回です。
今日やったことの把握ということで、これ
function foo(a,b,c,d) print(a,b,c,d) end function bar(a,b) return b,a end foo(1,2,bar(3,4))
を動かすことを目標にします。
luac でコンパイルするとこんなんになります。
0+ params, 6 slots, 0 upvalues, 0 locals, 6 constants, 2 functions 1 [3] CLOSURE 0 0 ; 00386298 2 [1] SETGLOBAL 0 -1 ; foo 3 [7] CLOSURE 0 1 ; 003865A0 4 [5] SETGLOBAL 0 -2 ; bar 5 [9] GETGLOBAL 0 -1 ; foo 6 [9] LOADK 1 -3 ; 1 7 [9] LOADK 2 -4 ; 2 8 [9] GETGLOBAL 3 -2 ; bar 9 [9] LOADK 4 -5 ; 3 10 [9] LOADK 5 -6 ; 4 11 [9] CALL 3 3 0 12 [9] CALL 0 0 1 13 [9] RETURN 0 1 function <test.lua:1,3> (7 instructions, 28 bytes at 00386298) 4 params, 9 slots, 0 upvalues, 4 locals, 1 constant, 0 functions 1 [2] GETGLOBAL 4 -1 ; print 2 [2] MOVE 5 0 3 [2] MOVE 6 1 4 [2] MOVE 7 2 5 [2] MOVE 8 3 6 [2] CALL 4 5 1 7 [3] RETURN 0 1 function <test.lua:5,7> (4 instructions, 16 bytes at 003865A0) 2 params, 4 slots, 0 upvalues, 2 locals, 0 constants, 0 functions 1 [6] MOVE 2 1 2 [6] MOVE 3 0 3 [6] RETURN 2 3 4 [7] RETURN 0 1
SETGLOBAL, GETGLOBAL, CALL, RETURN, MOVE, LOADK, CLOSURE 命令を実装すればいいことがわかりました。まだUpValue関係のソースを読んでいないので、CLOSUREは自由変数のない場合だけ動作するフェイク実装で済ませちゃいます。ではレッツトライ!
スタック
トップレベルのスクリプトをコンパイルしたprotoオブジェクトを渡すとそれを実行する、vmLoop関数を実装します。
function vmLoop(proto) -- スタックとその基本操作 local stk = {} local base = 1 local top = base + proto.maxstk local function getreg(n) return stk[base+n] end local function setreg(n,v) stk[base+n] = v end local function getregs(i,n) return unpack(stk,base+i,base+i+n-1) end local function setregs(i,n,...) local a = {...} local m = n if m > #a then m = #a end for j=1,m do stk[base+i+j-1] = a[j] end for j=m+1,n do stk[base+i+j-1] = nil end end
おさらいポイント
- baseとtopで現在の関数が使うスタックの範囲を表現
- baseから見てn番目の要素がn番レジスタ。そのgetterとsetter。
-- 呼び出し履歴スタック local cistk = {} local citop = 1 local function cipush(...) cistk[citop]={...} citop=citop+1 end local function cipop() citop=citop-1 return unpack(cistk[citop]) end
あんまり今回説明しませんでしたが、関数呼び出しの際にbaseやtopを保存する呼び出し履歴スタックです。
グローバル変数
-- グローバルテーブル local global_table = {print = print} local isfn = {}
global_tableというテーブルを用意して、ここにLuLu上で動くLuaのグローバル変数を入れることにしました。numberやstring、table、ライブラリ関数は、そのまま元々のLuaの値を入れることにします。今回のサンプルではライブラリ関数はprintしか使っていないので、printだけ入れておきます。
LuLu上のLua内で定義された関数は、特別なテーブルで表現します。isfnは判別用フラグ。
命令構造
-- メインの実行ループ local pc = 1 while true do local inst = proto.code[pc] pc = pc+1 local OP = bits(inst, 0,6) local A = bits(inst, 6,8) local C = bits(inst,14,9) local B = bits(inst,23,9) local Bx = B*2^9 + C local sBx = Bx - 131071 local RKB = getreg(B) if B>=2^8 then RKB = proto.kv[B-2^8+1] end local RKC = getreg(C) if C>=2^8 then RKC = proto.kv[C-2^8+1] end
bitsはビットマスク用に定義した補助関数です。おさらいポイント・・・
- 命令のビット形式は B-C-A-OP, Bx-A-OP, sBx-A-OP のどれか
- BとCは、最上位ビットが立っているときは定数表のインデックス、そうでないときはレジスタ
命令の実装
if OP == 0 then -- MOVE : R[A] = R[B] setreg(A, getreg(B)) elseif OP == 1 then -- LOADK : R[A] = ConstValue[Bx] setreg(A, proto.kv[Bx+1]) elseif OP == 5 then -- GETGLOBAL : R[A] = _G[ConstValue[Bx]] setreg(A, global_table[proto.kv[Bx+1]]) elseif OP == 7 then -- SETGLOBAL : _G[ConstValue[Bx]] = R[A] global_table[proto.kv[Bx+1]] = getreg(A)
この辺りは見たまんまの実装ですね。分岐はif文を連ねるのは格好悪いかなあーとも思うんですが、関数テーブルにしてディスパッチとかしてしまうとRETURNが綺麗にかけなかったのでひとまずこれで。
elseif OP == 36 then -- CLOSURE : R[A] = Funcs[Bx] -- ToDo: upvalues... setreg(A, {isfn=isfn, impl=proto.kf[Bx+1]}) end
CLOSURE命令は、UpValueの扱いは無視して、とにかく関数プロトタイプをレジスタに突っ込む処理と言うことにしてあります。
続いてCALL命令。
elseif OP == 28 then -- CALL : R[A],...,R[A+C-2], R[A]( R[A+1], ..., R[A+B-1] ) local fn = getreg(A) if type(fn)=="table" and rawget(fn, "isfn")==isfn then fn = rawget(fn, "impl") cipush(proto,pc,base,top,C-1) proto = fn pc = 1 local __top = B==0 and top or base+A+B base = base+A+1 top = base + proto.maxstk for i=__top,top-1 do stk[i+1] = nil end else setregs(A, C-1, fn(getregs(A+1, B-1))) end
おさらいポイント
elseif OP == 30 then -- RETRUN : R[A], ..., R[A+B-1] if citop == 1 then return else local __base, __top proto,pc,__base,__top,wanted = cipop() local numret = B==0 and top-A or B-1 if wanted == -1 then -- multival setregs(-1, numret, getregs(A,numret)) base,top = __base, base-1+numret else setregs(-1, wanted, getregs(A,numret)) base,top = __base, __top end end
RETURNも同じように、返す値の個数をtopかBで判断して、その分をコピー。baseやtopを元に復元。