データスタックの構造
まず、LuaのVMのスタックがどのように使われるかを調べます。前回ちょっと触れたように、LuaのVMは「レジスタマシン」といえども、スタックと別に特別な「レジスタ」があるわけではありません。ローカル変数や全てスタックに保存されて管理されます。いや、これはむしろ、スタックと思わずに レジスタ・ウィンドウ - Wikipedia と思った方がいいのかもしれません。関数呼び出しの時に自動的に退避されるレジスタです。
まず前回のおさらいから。スタックの状態を表現するのに使われている変数は4つあります。
typedef TValue *StkId; /* index to stack elements */ struct lua_State { ... StkId top; /* first free slot in the stack */ StkId base; /* base of current function */ ... StkId stack_last; /* last free slot in the stack */ StkId stack; /* stack base */ .. };
stack以上stack_last未満の範囲がスタック全体で、その一部、base以上top未満の部分を現在の関数が使用します。関数内ではbase[0]を0番レジスタ、base[1]を1番レジスタ、...、base[top-base-1]をtop-base-1番レジスタとして使うことができます。
// lvm.c / 命令iのオペランドAで指定されたレジスタにアクセスするマクロ #define RA(i) (base+GETARG_A(i))
コンパイル処理の方のソースを読んでいないので詳しくはわかりませんが、基本的には、Luaの変数1個につきレジスタ1個を割り当てるようですね。
CALL
スタックの細かい使い方を確認するために、関数呼び出し命令 CALL の実装を先回りして読んでみます。Cの関数を呼ぶ場合は特別なので、まずはLuaの関数を呼び出す場合のみを抜粋しました。
case OP_CALL: { int b = GETARG_B(i); int nresults = GETARG_C(i) - 1; }
CALL は3引数の命令で、CALL A B C は、次のような処理を実行します。(R[i]はi番レジスタを指すとする)
R[A],R[A+1],...,R[A+C-1] = R[A]( R[A+1], ..., R[A+B-2] )
B-1 が引数の数、C-1 が返値の数です。多値を使っていて引数の個数などが実行時までわからない場合は、B や C が 0 になっているようです。つまり、Lua VM は関数呼び出しのときにはこんな風にスタックを使います。
base ローカル変数等 V 関数呼び出し用の作業領域 top +------+------+-------+-------------------------------------------+-- | R[0] | R[1] | ... | 関数 | 引数1 | 引数2 | ... | 引数N | ... | +------+------+-------+-------------------------------------------+--
base付近はローカル変数の値を入れておく場所で、関数を呼びたくなったら、top近くに[関数][引数1]...[引数N]の順番で値をスタックに積みます(積む命令を実行します)。この状態にしてから、CALL V N+1 命令を実行する、と。続き・・・
if (b != 0) L->top = ra+b; /* else previous instruction set top */
ここで、topを引数リストの最後を指すように動かします。こんな感じ。
base ローカル変数等 V 関数呼び出し用の作業領域 top +------+------+-------+-------------------------------------------+-- | R[0] | R[1] | ... | 関数 | 引数1 | 引数2 | ... | 引数N | ... | +------+------+-------+-------------------------------------------+--
b == 0 は引数リストの最後が関数呼び出しの場合
foo(1, 2, bar(3, 4))
に使われる命令で、barが何個の値を返すかはbarのreturnの時にしかわからないので、fooを呼ぶ命令より前に、barのreturn命令がtopをうまいこと設定しておいてくれるそうです。あとでreturnの実装を見るときに確認しましょう。続きー
L->savedpc = pc; switch (luaD_precall(L, ra, nresults)) { case PCRLUA: { nexeccalls++; goto reentry; /* restart luaV_execute over new Lua function */ } ... }
現在のpc(命令列上の、今実行している命令のインデックス)を保存して、luaD_precallを呼びます。呼んだら、goto reentryでVMのループ関数の先頭に戻ります。luaD_precallの大ざっぱな流れはこうです。
// ldo.c int luaD_precall (lua_State *L, StkId func, int nresults) { ... cl = &clvalue(func)->l; L->ci->savedpc = L->savedpc; ...
まず、func(ra == Aレジスタの位置 == 関数オブジェクトが入っているレジスタ)からオブジェクトclを取り出し。pcはここでCallInfoのチェインに保存します。Luaで実装された関数だった場合・・・
if (!cl->isC) { /* Lua function? prepare its call */ ... if (!p->is_vararg) { /* no varargs? */ base = func + 1; if (L->top > base + p->numparams) L->top = base + p->numparams; }
次の関数のために、ここでbaseを動かします。baseは、func+1、つまり
base ローカル変数等 V ここ!!!! top +------+------+-------+-------------------------------------------+-- | R[0] | R[1] | ... | 関数 | 引数1 | 引数2 | ... | 引数N | ... | +------+------+-------+-------------------------------------------+--
ここを指すようにします。なので、呼ばれる関数からすると第一引数がR[0]、第二引数がR[1]、・・・に入っているようになりますね。呼ばれる関数側の受け取る引数の数p->numparamsよりも実際の呼び出しで渡された引数が多い場合は、余分な引数は無視してtopをちょっと左に戻します。
else { /* vararg function */ ... 可変長引数関数の場合、とりあえず略 ... } ci = inc_ci(L); /* now `enter' new function */ ci->func = func; L->base = ci->base = base; ci->top = L->base + p->maxstacksize; lua_assert(ci->top <= L->stack_last); L->savedpc = p->code; /* starting point */ ci->tailcalls = 0; ci->nresults = nresults;
ci(CallInfo)に色々情報を保存してます。ci->top が最終的に新しい関数にとってのtopになります。p->maxstacksizeに使用するレジスタの個数が入っているので、baseにそれを足しているだけですね。そして、
for (st = L->top; st < ci->top; st++) setnilvalue(st); L->top = ci->top; ... return PCRLUA; }
L->top(引数リストの最後)から使うスタック領域の最後 ci->top までを nil で初期化して、準備完了!です。
V 旧base V 新base V 旧top V 新top +------+------+----------------------------------------------+-- | R[0] | ... | 関数 | 引数1 | 引数2 | ... | 引数N | ... | +------+------+----------------------------------------------+--
RETURN
逆にRETURN命令はどうなってるでしょう。CALL命令とかなりよく似ています。
case OP_RETURN: { int b = GETARG_B(i); if (b != 0) L->top = ra+b-1; ... b = luaD_poscall(L, ra); ... if (b) L->top = L->ci->top; ... goto reentry; }
b-1が返す値の個数で、スタックの端っこにb-1個値を詰めた状態で、RETURN命令が実行されます。リストの最後が関数呼び出しの場合は、CALLの時と同じくb==0。これまたCALLと同じように、topをずらしてからluaD_poscallを呼び出します。poscallでは
// ldo.c int luaD_poscall (lua_State *L, StkId firstResult) { ... res = ci->func; /* res == final position of 1st result */ wanted = ci->nresults;
ci->funcで返値を置くべき場所を取得、ci->nresultsで呼び出し側が要求していた返値の個数を得ます。多値を返せる場合はnresults==wanted==-1なので、その場合wantedは無視するためにi>=0ではなくi!=0が停止条件になっています。
for (i = wanted; i != 0 && firstResult < L->top; i--) setobjs2s(L, res++, firstResult++); while (i-- > 0) setnilvalue(res++);
実際にその位置にコピーして(足りない場合はnilで埋めて)・・・
L->top = res; L->base = (ci - 1)->base; /* restore base */ L->savedpc = (ci - 1)->savedpc; /* restore savedpc */
topやbaseやpcを復元しつつ・・・(topにres(実際の返り値リストの最後)を入れてるのに注目!)
return (wanted - LUA_MULTRET); /* 0 iff wanted == LUA_MULTRET */
多値を返せる文脈で呼び出されていた場合 0 を返すようにして
b = luaD_poscall(L, ra);
if (b) L->top = L->ci->top;
多値じゃない文脈だと、topも、元のtopに復元します。多値を返している場合は、CALL のところで触れたように、RETURN が責任を持って top の位置をリストの最後に来るように調整しています。これでRETURN処理は完了。