1. ホーム
  2. スクリプト・コラム
  3. ルア

Luaのアサインメントタイプコードの説明

2022-02-12 11:44:32

次のソースコードを解析してバイトコードを生成するときに、lua vmがどのような処理を行うかを見てみましょう。

 foo = "bar"
 local a, b = "a", "b"
 foo = a


まず、ChunkySpyツールを使って、vmが最終的にどのような命令を生成するかを正確に見てみましょう。

ここで、[number]で始まる行が実際にvmが生成するバイトコードで、合計6行のバイトコードが生成されていることがわかります。まず loadk は定数テーブルの 1 で始まる定数、つまり "bar" をレジスタ 0 に代入し、次に setglobal はレジスタ 0 の内容をグローバル変数テーブルの 0 で始まるグローバル変数、つまり foo に代入しています。ここで、レジスタ0と1は現在の関数のローカル変数、つまり変数aとbです。最後にsetglobalはグローバル変数fooに割り当てられた変数aの値になります。最後のreturn01は各チャンクの最後にvmが生成されて、何も使うものがありません。これで、lua vmが生成するバイトコードの意味が理解できたと思いますが、次にvmがどのように、そしてなぜこれらのバイトコードを生成するのかを見ていきましょう。

luaL_dofile関数でluaスクリプトのソースコードを実行するとき、2つの段階があります。1つ目はスクリプトをメモリにロードし、パースしてバイトコードを生成し、luaスタックの一番上にあるメインチャンクにラップすること、2つ目はlua_pcallを呼び出してチャンクを実行することです。

数回前の記事で紹介したように、dofileがluaY_parserという関数にぶつかると

Proto *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, const char *name) {
 struct LexState lexstate;
 struct FuncState funcstate;
 -- ... ...
 funcstate.f->is_vararg = VARARG_ISVARARG; /* main func. is always vararg */
 luaX_next(&lexstate); /* read first token */
 chunk(&lexstate);
 -- ... ...
 return funcstate.f;
}


luaY_parser関数の最初の2行はLexStateとFuncState構造体変数を定義しています。LexStateは現在の字句解析状態情報だけでなく、コンパイルシステム全体のグローバル状態も格納し、FuncState構造体は現在の関数コンパイル状態データを格納するために使用されます。luaのソースコードでは、グローバルな関数の実行本体、つまりメインfuncがあり、解析の開始時に、現在の関数は、メインfunc関数でなければならない、この時点でfuncstateの3行目は、この関数の状態を示しています。この関数は必然的に不定形のパラメータを受け取ることになるので、5行目はis_vargロゴをVARARGに設定し、6行目でluaX_nextがファイルストリームを解析して最初のトークンを分離し、lexstateのtメンバーに保存します(tは " foo" グローバル変数です。そして chunk 関数が呼び出され、ここから再帰的降下構文解析の全プロセスが開始されます。

static void chunk (LexState *ls) {
 /* chunk -> { stat [`;'] } */
 int islast = 0;
 enterlevel(ls);
 while (!islast && !block_follow(ls->t.token)) {
  islast = statement(ls);//recursive descent point
  testnext(ls, ';');
  lua_assert(ls->fs->f->maxstacksize >= ls->fs->freereg &&
        ls->fs->freereg >= ls->fs->nactvar);
  ls->fs->freereg = ls->fs->nactvar; /* free registers */
 }
 leavelevel(ls);
}


luaにはスコープレベルという概念があるので、あるレベルに入るときにはenterlevel関数が、現在のレベルから離れるときにはleavelevel関数が呼ばれます。まず、whileループに入ると、カレントトークンは"foo"で、これはエンドマーカーでもブロックスタート語彙でもないので、statement関数に入り、その本体は長いswitch... . ...ケース... ステートメント関数の本体は長い switch...case... コード構造で、最初のトークンによって異なる呼び出し解決ブランチに入ります。この例では、デフォルトのブランチに入ります。

static int statement (LexState *ls) {
 -- ... ...
 switch (ls-> t.token) {
  case TK_IF: { /* stat -> ifstat */
   ifstat(ls, line);
   return 0;
  }
  case TK_WHILE: { /* stat -> whilestat */
   whilestat(ls, line);
   return 0;
  }
  -- ... ...
  default: {
   exprstat(ls);
   return 0; /* to avoid warnings */
  }
 }
}


exprstate関数を入力します。

static void exprstat (LexState *ls) {
 /* stat -> func | assignment */
 FuncState *fs = ls-> fs;
 struct LHS_assign v;
 primaryexp(ls, &v.v);
 if (v.v.k == VCALL) /* stat -> func */
  SETARG_C(getcode(fs, &v.v), 1); /* call statement uses no results */
 else { /* stat -> assignment */
  v.prev = NULL;
  assignment(ls, &v, 1);
 }
}


4行目の LHS_assign 構造体は、a,b,c = ...のように複数の変数が代入される場合を想定したものである。LHS_assign のメンバ v 型 expdesc には、等号の左側の変数を記述します。詳細は、前回の expdesc の記事を参照してください。次に、primaryexp で "foo" 変数の expdesc 情報を取得して記入し、それを prefixexp 関数に渡します。

 static void prefixexp (LexState *ls, expdesc *v) {
  /* prefixexp -> NAME | '(' expr ')' */
  switch (ls-> t.token) {
   case '(': {
    int line = ls->linenumber;
    luaX_next(ls);
    expr(ls, v);
    check_match(ls, ')', '(', line);
    luaK_dischargevars(ls->fs, v);
    return;
   }
   case TK_NAME: {
    singlevar(ls, v);
    return;
   }
   default: {
    luaX_syntaxerror(ls, "unexpected symbol");
    return;
   }
  }
 }


現在のトークンは "foo" なので、TK_NAME ブランチに移動して singlevar を呼び出します。

static void singlevar (LexState *ls, expdesc *var) {
 TString *varname = str_checkname(ls);
 FuncState *fs = ls->fs;
 if (singlevaraux(fs, varname, var, 1) == VGLOBAL)
  var->u.s.info = luaK_stringK(fs, varname); /* info points to global name */
}
static int singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) {
 if (fs == NULL) { /* no more levels?
  init_exp(var, VGLOBAL, NO_REG); /* default is global variable */
  return VGLOBAL;
 }
 else {
  int v = searchvar(fs, n); /* look up at current level */
  if (v >= 0) {
   init_exp(var, VLOCAL, v);
   if (!base)
    markupval(fs, v); /* local will be used as an upval */
   return VLOCAL;
  return VLOCAL; }
  else { /* not found at current level; try upper one */
   if (singlevaraux(fs->prev, n, var, 0) == VGLOBAL)
    return VGLOBAL;
   var->u.s.info = indexupvalue(fs, n, var); /* else was LOCAL or UPVAL */
   var->k = VUPVAL; /* upvalue in this level */
   return VUPVAL;
  }
 }


singlevaraux関数は、変数がローカルか、アップバリューか、グローバルかを判断します。fs が null ならばその変数はグローバルで、そうでなければ searchvar に行って現在の関数のローカル変数の配列を探し、そうでなければ fs の prev メンバーに基づいてその親関数の FuncState を取得して singlevaraux に渡して再帰的に探し、前のものに合致しないならその変数は upvlaue になります。この場合、21行目まで行くと、fsはすでにmain funcを指しているので、そのprevはnullとなり、"foo"はグローバルと判定されてexprstate関数に返されます。fsの情報を取得した後、"foo"は関数呼び出しではないので、代入関数に進みます。

primaryexp(ls, &v.v);
 if (v.v.k == VCALL) /* stat -> func */
  SETARG_C(getcode(fs, &v.v), 1); /* call statement uses no results */
 else { /* stat -> assignment */
  v.prev = NULL;
  assignment(ls, &v, 1);
 }


代入関数では、まず次のトークンが "," かどうかを判断し、この場合ではなく、それは単一変数の代入であることを意味し、次に次のトークンが "=" です。 右側には、いくつかの値を持って、この例では、1であり、その後、左側の変数の数が右側の値の数に等しいかどうかを判断します、調整するadjust_assign関数を入力する等しくない、この例では、順番にluaK_setoneretとluaK_storevar関数を入力するので、等しいです。luaK_storevarで最初の入力int e = luaK_exp2anyreg(fs、元);関数luaK_exp2anyreg Kはこの関数がバイトコード関連関数、値"バー&quot用ex、この関数とexの種類によって異なるバイトコードを生成discharge2reg、呼び出します表しています。

static void discharge2reg (FuncState *fs, expdesc *e, int reg) {
 luaK_dischargevars(fs, e);
 switch (e->k) {
  case VNIL: {
   luaK_nil(fs, reg, 1);
   break;
  }
  case VFALSE: case VTRUE: {
   luaK_codeABC(fs, OP_LOADBOOL, reg, e->k == VTRUE, 0);
   break;
  }
  case VK: {
   luaK_codeABx(fs, OP_LOADK, reg, e->u.s.info);
   break;
  }
//... ...
}


"bar"は定数なので、luaK_codeABx関数を呼び出してloadkバイトコードを生成します。regはロードした定数値を保持するレジスタ番号、e->u.s.infoは値の種類によって異なる意味を表し、コメントによるとinfoはこの時定数配列の添え字と分かります。添え字です。

typedef enum {
 //... ...
 VK, /* info = index of constant in `k' */
 VKNUM, /* nval = numerical value */
 VLOCAL, /* info = local register */
 VGLOBAL, /* info = index of table; aux = index of global name in `k' */
 //... ...
} expkind;


上記の関数にloadkの戻り値を生成した後、luaK_codeABx(fs, OP_SETGLOBAL, e, var->u.s.info); ここでeは luaK_exp2anyreg の戻り値で、定数が格納されているレジスタマーカー、コメントによると情報グローバル型の場合はグローバルテーブル対応の添字、したがって luaK_codeABx 関数が setglobal バイトコードを生成します、ちょうど loadk を使用して定数をレジスタ値で読み込んでグローバルテーブル対応の場所を保存するようにします。したがって、foo = "bar" 文は、対応するバイトコード全体を生成します。

次に、local a,b = "a","b" 文のバイトコードを生成します。a,b がローカル変数であることと、代入文が多変数代入文であることを除けば、手順はほぼ同じで、前の関数では LHS_assign チェーンを使って a,b 変数を結びつけています。図に示すように

今回の記事は以上です。