ひとり勉強会

ひとり楽しく勉強会

データスタックの構造

まず、LuaVMのスタックがどのように使われるかを調べます。前回ちょっと触れたように、LuaVMは「レジスタマシン」といえども、スタックと別に特別な「レジスタ」があるわけではありません。ローカル変数や全てスタックに保存されて管理されます。いや、これはむしろ、スタックと思わずに レジスタ・ウィンドウ - 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処理は完了。