ひとり勉強会

ひとり楽しく勉強会

おまけ: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

おさらいポイント

  • Luaの関数だった場合、baseを引数リストの先頭(A+1) にもっていく
  • 引数の終わりはB==0(多値)の場合top、そうでないばあいA+Bでわかるので、それより後ろをnilでクリア
    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を元に復元。

今日の成果

> luac test.lua

> lua lulu.lua luac.out
1       2       4       3

できた!今日のソース →lulu2.zip