Lua源码阅读:命令的解析与执行

具体分析文档如何被解析、生成对应命令以及到虚拟机执行的流程。

[](https://bitzhangmo.github.io/#Lua%E8%AF%8D%E6%B3%95%EF%BC%9A

“Lua词法:”)Lua词法:

Lua使用一遍扫描代码文档的方式生成字节码,即在第一遍扫描代码的时候就生成字节码了,这么做主要是为了加快解释执行的速度。

### 赋值类命令:

#### 局部变量:

lua代码:

1  
local a = 10  

—|—

相关的EBNF词法:

1  
2  
3  
4  
5  
6  
7  
chunk -> { stat [`;'] }  
stat -> localstat  
localstat -> LOCAL NAME {`,' NAME}[`=' explist1]  
explist1 -> expr {`,' expr}  
exp -> subexpr  
subexpr -> simpleexp  
simpleexp -> NUMBER  

—|—

第3行为赋值,涉及几个问题:

  • = 左边是一个变量,只有变量才能赋值,于是涉及以下问题:如何存储局部变量,如何查找变量,怎么确定一个变量是局部变量、全局变量还是UpValue?
  • = 右边是一个表达式列表 explist1,在这个最简单的例子中,这个表达式是常量数字 10。这种情况很简单,如果不是一个立即能得到的值,比如是一个函数调用的返回值,或者是对这个 block 之外的其他变量的引用,又怎么处理呢?

第一个问题,如何识别局部变量?

首先在函数 localstat 中,会有一个循环调用函数 new_localvar,将=左边的所有以逗号分隔的变量都生成一个相应的局部变量。

存储每个局部变量的信息时,我们使用的是 LocVar 结构体:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
*/  
typedef struct  {  
  TString *varname;  
  int startpc;  /* first point where variable is active */  
  int endpc;    /* first point where variable is dead */  
} LocVar;  

—|—

其中变量名放在 LocVar 结构体的变量 varname 中。函数中所有局部变量的 LocVar 信息,一般存放在 Proto 结构体的 LocVar 中。

在结构体FuncState 中,成员变量 freereg 存放的就是当前函数栈的下一个可用位置。

在 Lua5.3 版本中,我找到了一个类似的函数:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
// lparser.c  
static void statlist (LexState *ls) {  
  /* statlist -> { stat [';'] } */  
  while (!block_follow(ls, 1)) {  
    if (ls->t.token == TK_RETURN) {  
      statement(ls);  
      return;  /* 'return' must be last statement */  
    }  
    statement(ls);  
  }  
}  

—|—

这里调用 statement()函数:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
static void statement (LexState *ls) {  
  int line = ls->linenumber;  /* may be needed for error messages */  
  enterlevel(ls);  
  switch (ls->t.token) {  
    case TK_LOCAL: {  /* stat -> localstat */  
      luaX_next(ls);  /* skip LOCAL */  
      if (testnext(ls, TK_FUNCTION))  /* local function? */  
        localfunc(ls);  
      else  
        localstat(ls);  
      break;  
    }  
  }  
  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);  
}  

—|—

可以看到在第 9 行对当前函数栈存放的变量数量(包括函数的局部变量、函数的参数等)进行调整。

那么,nactvar 这个变量又是何时调整的呢?在这个例子中,变量 a 是一个局部变量,最后会在解析局部变量的函数 adjustlocalvars 中进行调整:

1  
2  
3  
4  
5  
6  
7  
static void adjustlocalvars (LexState *ls, int nvars) {  
  FuncState *fs = ls->fs;  
  fs->nactvar = cast_byte(fs->nactvar + nvars);  
  for (; nvars; nvars--) {  
    getlocvar(fs, fs->nactvar - nvars)->startpc = fs->pc;  
  }  
}  

—|—

至此,第一个问题得到了解决:在函数 localstat 中,会读取=号左边的所有变量,并在 Proto 结构体中创建相应的局部变量信息;而变量在 Lua 函数栈中的存储位置存放在 freereg 变量中,它会根据当前函数栈中变量的数量进行调整。

第二个问题:表达式的结果如何存储?

解析表达式的结果会存储在一个临时数据结构 expdesc 中:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
typedef struct expdesc {  
  expkind k;  
  union {  
    struct {  /* for indexed variables (VINDEXED) 索引变量(VINDEXED) */  
      short idx;  /* index (R/K) 索引 */  
      lu_byte t;  /* table (register or upvalue) 表(寄存器或者上值) */  
      lu_byte vt;  /* whether 't' is register (VLOCAL) or upvalue (VUPVAL) t 是寄存器(局部变量)或者上值(上值) */  
    } ind;  
    int info;  /* for generic use */  
    lua_Number nval;  /* for VKFLT */  
    lua_Integer ival;    /* for VKINT */  
  } u;  
  int t;  /* patch list of 'exit when true' */  
  int f;  /* patch list of 'exit when false' */  
} expdesc;  

—|—

  • 变量 k 表示具体的类型
  • 后面紧跟的 union u 根据不同的类型存储的数据有所区分,具体可以看 expkind 类型定义后面的注释
  • 至于 t 和 f 这两个变量,目前可以暂时不管,这是跳转相关的命令。

#### 解析表达式列表:

解析表达式列表的函数为explist:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
static int explist (LexState *ls, expdesc *v) {  
  /* explist -> expr { ',' expr } */  
  int n = 1;  /* at least one expression */  
  expr(ls, v);  
  while (testnext(ls, ',')) {  
    luaK_exp2nextreg(ls->fs, v);  
    expr(ls, v);  
    n++;  
  }  
  return n;  
}  

—|—

  • 调用函数expr解析表达式
  • 当解析的表达式列表中还存在其他的表达式时,即有逗号(,)分隔的式子时,针对每个表达式继续调用expr函数解析表达式,将结果缓存在expdesc结构体中,然后调用函数 luaK_exp2nextreg 将表达式存入当前函数的下一个可用寄存器中。

根据上面的调用路径,最终会走入 simpleexp 中:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
17  
18  
static void simpleexp (LexState *ls, expdesc *v) {  
  /* simpleexp -> FLT | INT | STRING | NIL | TRUE | FALSE | ... |  
                  constructor | FUNCTION body | suffixedexp */  
  switch (ls->t.token) {  
		...  
    case TK_INT: {  
      init_exp(v, VKINT, 0);  
      v->u.ival = ls->t.seminfo.i;  
      break;  
    }  
		...  
    default: {  
      suffixedexp(ls, v);  
      return;  
    }  
  }  
  luaX_next(ls);  
}  

—|—

  • 使用类型 VKINT 初始化 expdesc 结构体,这个类型表示数字常量
  • 将具体的数据赋值给 expdesc 结构体中的 ival,前面说过,expdesc 结构体中 union u 的数据根据不同的类型会存储不同的信息,在 VKINT 这个类型下就是用来存储数字的。

现在这个表达式的信息已经存放在 expdesc 结构体中,需要进一步根据这个结构体的信息来生成对应的字节码。

这个工作由函数 luaK_exp2nextreg 完成,需要根据 expdesc 结构体生成字节码时,都要经过它:

  • 调用 luaK_dischargevars 函数,根据变量所在的不同作用域(local,global,upvalue)来决定这个变量是否需要重定向
  • 调用 luaK_reserveregs 函数,分配可用的函数寄存器空间,得到这个空间对应的寄存器索引。有了空间,才能存储变量
  • 调用exp2reg 函数,真正完成把表达式的数据放入寄存器空间的工作。在这个函数中,最终又会调用dischargereg函数,这个函数式根据不同的表达式类型(NIL,布尔表达式,数字等)来生成访问表达式的值到寄存器的字节码

在函数 discharge2reg 中最终会走到这里:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
13  
14  
15  
16  
static void discharge2reg (FuncState *fs, expdesc *e, int reg) {  
  luaK_dischargevars(fs, e);  
  switch (e->k) {  
		...  
    case VKINT: {  
      luaK_codek(fs, reg, luaK_intK(fs, e->u.ival));  
      break;  
    }  
    default: {  
      lua_assert(e->k == VVOID || e->k == VJMP);  
      return;  /* nothing to do... */  
    }  
  }  
  e->u.info = reg;  
  e->k = VNONRELOC;  
}  

—|—

在这个函数的参数中,reg 参数就是前面得到的寄存器索引,于是最后根据 k 值生成了OP_LOADK命令(另一种情况是生成OP_LOADKX命令),将数字 10 加载到 reg 参数对应的寄存器中。

至此,通过对这个最简单的向局部变量赋值操作的分析,完成了 Lua 解释器从词法分析到生成字节码的全过程分析:

  • 每个局部变量都有一个对应的 LocVar 结构体存储它的变量名信息。
  • 每个局部变量都会对应分配一个函数栈的位置来保存它的数据。
  • 解析表达式的结果会存在 expdesc 结构体中。根据不同的类型,内部使用的联合体存放的数据有不同的意义。
  • luaK_exp2nextreg 是一个非常重要的函数,它用于将 expdesc 结构体的信息中存储的表达式信息转换成对应的 opcode。

[](https://bitzhangmo.github.io/#%E6%B5%81%E7%A8%8B%E5%9B%BE%EF%BC%9A

“流程图:”)流程图:

mh9OQU.png

如果代码变为:

1  
local a,b = 10  

—|—

则在localstat函数中进入:

1  
2  
3  
4  
5  
static void localstat (LexState *ls) {  
	...  
  adjust_assign(ls, nvars, nexps, &e);  
  adjustlocalvars(ls, nvars);  
}  

—|—

第一个函数adjust_assign用于根据等号两边变量和表达式的数量来调整赋值。具体来说,在上面这个例子中,当变量数量多于等号右边的表达式数量时,会将多余的变量置为NIL。

第二个函数adjustlocalvars会根据变量的数量调整FuncState结构体中记录局部变量数量的nactvar对象,并记录这些局部变量的startpc值。

如果代码变为:

1  
2  
local a = 10  
local b = a  

—|—

主要区别在于走到simpleexp函数时,进入的是另一条路径,走入了primaryexp函数中。然后在prefixexp函数中,判断这是一个变量时,会调用singlevar函数(实际上,最后会调用递归函数 singlevaraux)来进行变量的查找,这个函数的大体流程如下:

  1. 如果变量在当前函数的 LocVar 结构体数组中找到,那么这个变量就是局部变量,类型为 VLOCAL。
  2. 如果在当前函数中找不到,就逐层往上面的 block 来查找,如果在某一层查到了,那么这个变量就是 UpValue,类型为 VUPVAL。
  3. 如果最后那层都没有查到,那么这个变量就是全局变量,类型为 VGLOBAL。

在 luaK_dischargevars 函数中,根据三种不同的类型有不同的操作。

如果赋值的源数据是局部变量,则使用 MOVE 命令来完成赋值。

#### 全局变量:

1  
2  
a = 10  
local b = a  

—|—

这时对应的 luaK_dischargevars 函数这样:

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
12  
void luaK_dischargevars (FuncState *fs, expdesc *e) {  
  switch (e->k) {  
		...  
    case VGLOBAL: {  
      e->u.s.info = luaK_codeABx(fs, OP_GETGLOBAL, 0, e->u.s.info);  
      e->k = VRELOCABLE;  
      break;  
    }  
    ...  
  }  
}  
// lua 5.1.5  

—|—

而在 Lua 5.3版本中,应该是在这部分函数中对全局变量进行处理的( 存疑 )。

1  
2  
3  
4  
5  
6  
7  
8  
9  
10  
11  
static void singlevar (LexState *ls, expdesc *var) {  
  TString *varname = str_checkname(ls);  
  FuncState *fs = ls->fs;  
  if (singlevaraux(fs, varname, var, 1) == VVOID) {  /* global name? */  
    expdesc key;  
    singlevaraux(fs, ls->envn, var, 1);  /* get environment variable */  
    lua_assert(var->k == VLOCAL || var->k == VUPVAL);  
    codestring(ls, &key, varname);  /* key is variable name */  
    luaK_indexed(fs, var, &key);  /* env[varname] */  
  }  
}  

—|—

参考:

  1. 扩展巴科斯范式

糖果

糖果
LUA教程

Lapis框架的常用处理方法

Lapis框架的常用处理方法 Continue reading

MoonScript实现选择排序

Published on February 26, 2017

MoonScript与Redis客户端

Published on January 19, 2017