Skip to content

Latest commit

 

History

History
526 lines (366 loc) · 26.7 KB

ch04-赋值类指令.md

File metadata and controls

526 lines (366 loc) · 26.7 KB

赋值类指令

这一节首先从赋值类指令讲起,将会通过最简单的表达式开始,逐渐变化来分析各种赋值类指令的行为。

局部变量赋值

先来讨论最简单的局部变量赋值,其对应的Lua代码如下:

local a = 10

回到前面提过的Lua的EBNF词法,来看看解析这一句代码,Lua解释器都走过了哪些函数,这里仅截取与这行代码相关的部分:

chunk -> { stat [`;'] }
stat -> localstat
localstat -> LOCAL NAME {`,' NAME} [`=' explist1]
explist1 -> expr {`,' expr}
exp -> subexpr
subexpr -> simpleexp
simpleexp -> NUMBER

这里面涉及到几个问题:

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

先来解决第一个问题,即如何识别存放局部变量。

首先看到在函数localstat中,首先会有一个循环调用函数new_localvar,将“=”左边的所有以","分隔的变量都生成一个相应的局部变量。 每一个局部变量,存储它的信息时使用的是LocVar结构体:

(lobject.h)
262 typedef struct LocVar {
263   TString *varname;
264   int startpc;  /* first point where variable is active */
265   int endpc;    /* first point where variable is dead */
266 } LocVar;

这里主要存储了变量名,放在该结构体的变量varname中。一个函数的所有局部变量的LocVar信息,是存放在Proto结构体的locvars中。

至此,第一个问题得到了解决:在函数localstat中,会读取“=”号左边的所有变量,创建相应的局部变量信息在Proto结构体中。

再来看第二个问题,表达式的结果如何存储?

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

(lparser.h)
 37 typedef struct expdesc {
 38   expkind k;
 39   union {
 40     struct { int info, aux; } s;
 41     lua_Number nval;
 42   } u;
 43   int t;  /* patch list of `exit when true' */
 44   int f;  /* patch list of `exit when false' */
 45 } expdesc;

其中:

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

有了对数据结构expdesc的了解,回到解析表达式列表的函数explist1中,它主要的工作是以下几个:

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

我们来看看针对这里的例子,上面的步骤都是如何进行的。

在我们的例子中,“=”右边的式子是数字10,根据调用路径,它最终会走入函数simpleexp中:

(lparser.c)
 727 static void simpleexp (LexState *ls, expdesc *v) {
 728   /* simpleexp -> NUMBER | STRING | NIL | true | false | ... |
 729                   constructor | FUNCTION body | primaryexp */
 730   switch (ls->t.token) {
 731     case TK_NUMBER: {
 732       init_exp(v, VKNUM, 0);
 733       v->u.nval = ls->t.seminfo.r;
 734       break;
 735     }

这里做的就是两个工作:

  1. 使用类型VKNUM初始化expdesc结构体。
  2. 将具体的数据也就是这里的10赋值给expdesc结构体中的nval,前面提过,expdesc结构体中的union u的数据,根据不同的类型会存储不同的信息,在VKNUM这个类型下就是用来存储数字的。

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

这个工作由函数luaK_exp2nextreg完成,这是一个非常重要的函数,原因就在于任何根据expdesc结构体需要生成opcode的时候,都需要经过它。它做了如下几个工作:

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

luaK_exp2nextreg函数非常重要,这里只是做简单的描述,后面还会陆续展开这个函数其他相关的工作。

回到这个例子中,这里的表达式是数字10,于是在函数discharge2reg中最终会走到这里:

(lcode.c)
343 static void discharge2reg (FuncState *fs, expdesc *e, int reg) {


358     case VKNUM: {
359       luaK_codeABx(fs, OP_LOADK, reg, luaK_numberK(fs, e->u.nval));
360       break;
361     }

在这个函数的参数中,reg参数就是前面第二步得到的寄存器索引,于是最后就生成了LOADK指令,将数字10加载到reg参数对应的寄存器中。

到了这里,就完成了第一个最简单的给局部变量赋值的从词法分析到生成opcode的全过程分析。稍微来做一个小结:

  1. 所有局部变量都会有一个对应的LocVar结构体存储它的变量名信息。
  2. 解析表达式的结果会存在expdesc结构体中,这个结构体中根据不同的类型,内部使用的联合体存放的数据有不同的意义。
  3. luaK_exp2nextreg是个非常重要的函数,它完成的工作是将expdesc结构体的信息中存储的表达式信息转换为对应的opcode。

前面提过,还会有查找变量的过程,但是这里并没有出现,因为在这个例子中,赋值的操作最后转换为了加载数据到寄存器的操作。在后面涉及到变量间的赋值中,我们再做这部分的解说。

我们再把上面的例子做扩充,如果变成下面这样会有哪些不同:

local a,b = 10

这个例子与前面的不同在于,”=“左右两边的表达式数量并不相等。这需要回头看看前面的localstat函数,这个函数最后

(lparser.c)
1179 static void localstat (LexState *ls) {

1193   adjust_assign(ls, nvars, nexps, &e);
1194   adjustlocalvars(ls, nvars);
1195 }

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

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

再对前面的例子做变化,这次变成了变量之间的赋值:

local a = 10
local b = a

第一句代码前面已经解释过了,第二句的前半部分跟前面一样,只是等号右边的表达式是一个局部变量a,来看看这里有什么变化。

这行代码走过的路径如下:

chunk -> { stat [`;'] }
stat -> localstat
localstat -> LOCAL NAME {`,' NAME} [`=' explist1]
explist1 -> expr {`,' expr}
exp -> subexpr
subexpr -> simpleexp
simpleexp -> primaryexp
primaryexp -> prefixexp
prefixexp -> NAME

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

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

在我们的例子中,等号左边的表达式是变量a,这是一个局部变量。

上面完成了前面关于查找变量的过程说明,下面来分析与前面相比将表达式赋值到寄存器又哪些不同,就是前面提过的luaK_exp2nextreg为入口的整个流程。

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

304 void luaK_dischargevars (FuncState *fs, expdesc *e) {
305   switch (e->k) {
306     case VLOCAL: {
307       e->k = VNONRELOC;
308       break;
309     }

如果一个变量是VLOCAL,那么说明前面已经看到过这个变量出现了,就是这里的局部变量a,它在第一行代码中已经出现了,那么它既是不需要重定向的,也是不需要额外的语句把这个值加载进来的,于是把它的类型修改为VNONRELOC。

接着在函数discharge2reg中,

343 static void discharge2reg (FuncState *fs, expdesc *e, int reg) {

367     case VNONRELOC: {
368       if (reg != e->u.s.info)
369         luaK_codeABC(fs, OP_MOVE, reg, e->u.s.info, 0);
370       break;
371     }

如果一个表达式类型是VNONRELOC,也就是不需要重定位的,那么直接生成MOVE指令来完成变量的赋值。

那么,什么是重定向,以及为什么需要重定向呢?把前面的例子做一个修改:

a = 10
local b = a

此时,变量a就不是一个局部变量,而是一个全局变量了,那么对应的在前面的luaK_dischargevars函数中就是这样的:

304 void luaK_dischargevars (FuncState *fs, expdesc *e) {
305   switch (e->k) {

315     case VGLOBAL: {
316       e->u.s.info = luaK_codeABx(fs, OP_GETGLOBAL, 0, e->u.s.info);
317       e->k = VRELOCABLE;
318       break;
319     }

与前面VLOCAL类型不同的是:

  1. 首先生成了一个OP_GETGLOBAL指令,用于获取全局变量的值到当前函数的寄存器中。
  2. 将类型修改为VRELOCABLE,即类型为重定向。

之所以需要重定向,是因为当生成这个OP_GETGLOBAL指令时,并不知道当前可用的寄存器地址是什么,需要到后面的discharge2reg函数中才知道,因为在这里寄存器地址作为这个函数的reg参数传入了:

343 static void discharge2reg (FuncState *fs, expdesc *e, int reg) {

345   switch (e->k) {

362     case VRELOCABLE: {
363       Instruction *pc = &getcode(fs, e);
364       SETARG_A(*pc, reg);
365       break;
366     }

当一个变量类型是重定向时,根据reg参数来写入这个指令的参数A。

全局变量赋值

分析全局变量赋值时使用的Lua语句如下:

	a = 10

这里涉及到两个操作,由于"="号右边的表达式是常量,首先将通过loadk指令将这个常量赋值到栈中.其次,由于表达式左边的变量a是全局变量,因此还要通过setglobal指令将上一步已经赋值到栈的常量赋值给这个全局变量.

来看看这两条指令的具体格式:

OP_LOADK,/*	A Bx	R(A) := Kst(Bx)	*/

OP_SETGLOBAL,/*	A Bx	Gbl[Kst(Bx)] := R(A) */

loadk指令涉及到两个变量,变量A表示栈地址,而变量Bx表示Kst数组也就是常量数组的索引.

setglobal同样涉及到两个变量,这两个变量的作用于loadk指令相同,所不同的是还需要去操作全局变量表.需要注意的是,Gbl是一个表,以字符串为索引,而Kst则是一个数组,里面存放的时常量,因此"Gbl[Kst(Bx)]"的涵义就是:先找到该全局变量的变量名,然后再使用该变量名在全局表中得到全局变量.

这条语句,在使用Lua的递归下降语法分析器时,调用的函数路径依次是:

chunk
	statement
		exprstat
			primaryexp
				prefixexp
					singlevar
						singlevaraux
							searchvar
							init_exp
							markupval
							indexupvalue
			assignment
				luaK_exp2nextreg
				luaK_storevar

其重点有两个地方,一是如何识别变量a,其次是如何给变量a赋值常量10.

查找识别变量

chunk函数会首先进入statement函数中,statement函数会根据下一个将处理的token类型,来进行下一步的工作,比如如果是关键字"if",将进入ifstat函数中进行处理.但是在这个实例代码中,下一个待处理的token是id为"a"的变量,于是就进入了exprstat函数中,这个函数用于处理表达式赋值或者函数调用这样的操作.

在exprstat函数中,看到了LHS_assign结构体,其定义如下:

(lparser.c)
892 /*
893 ** structure to chain all variables in the left-hand side of an
894 ** assignment
895 */
896 struct LHS_assign {
897   struct LHS_assign *prev;
898   expdesc v;  /* variable (global, local, upvalue, or indexed) */
899 };

这个结构体是用来表示赋值时等号左边的表达式所用,成员prev指向在同一个表达式中的上一个LHS_assign结构体指针,而v则真正用来存储表达式的信息.

例如这样的赋值:

a,b = 1, 2

那么这里假设有LHS_assign数据a_data,b_data分别存储了a,b的信息的话,则其中b_data->prev = a_data.

再来看expdesc结构体的定义:

(lparser.h)
37 typedef struct expdesc {
38   expkind k;
39   union {
40     struct { int info, aux; } s;
41     lua_Number nval;
42   } u;
43   int t;  /* patch list of `exit when true' */
44   int f;  /* patch list of `exit when false' */
45 } expdesc;

该结构体中,k表示该表达式的类型,u是一个union为了节省空间之用,nval用来存储数据为数字的情况,自不必多解释,而info和aux根据不同的数据类型各自表示不同的信息,这些信息都可以在expkind enum的注释中看到:

(lparser.h)
19 typedef enum {
20   VVOID,  /* no value */
21   VNIL,
22   VTRUE,
23   VFALSE,
24   VK,   /* info = index of constant in `k' */
25   VKNUM,  /* nval = numerical value */
26   VLOCAL, /* info = local register */
27   VUPVAL,       /* info = index of upvalue in `upvalues' */
28   VGLOBAL,  /* info = index of table; aux = index of global name in `k' */
29   VINDEXED, /* info = table register; aux = index register (or `k') */
30   VJMP,   /* info = instruction pc */
31   VRELOCABLE, /* info = instruction pc */
32   VNONRELOC,  /* info = result register */
33   VCALL,  /* info = instruction pc */
34   VVARARG /* info = instruction pc */
35 } expkind;

以这里的例子为例,变量a在这里是全局变量,于是它对应的expkind是VGLOBAL.

expdesc结构体中的t和f这两个变量在这里暂时用不到,留待后面继续解释.

前面解释了LHS_assign和expdesc结构体的含义,现在继续回到exprstat函数中,它会调用primaryexp函数来获取该表达式的expdesc结构体信息,而这会最终走入prefixexp函数中,由于这里要处理的token是a,它是一个TK_NAME类型的token,所以会进入singlevar函数中查找该变量到底是GLOBAL/LOCAL/UPVAL.

查找一个变量,主要的逻辑在函数singlevaraux,来看看它的代码:

(lparser.c)
224 static int singlevaraux (FuncState *fs, TString *n, expdesc *var, int base) {
225   if (fs == NULL) {  /* no more levels? */
226     init_exp(var, VGLOBAL, NO_REG);  /* default is global variable */
227     return VGLOBAL;
228   }
229   else {
230     int v = searchvar(fs, n);  /* look up at current level */
231     if (v >= 0) {
232       init_exp(var, VLOCAL, v);
233       if (!base)
234         markupval(fs, v);  /* local will be used as an upval */
235       return VLOCAL;
236     }
237     else {  /* not found at current level; try upper one */
238       if (singlevaraux(fs->prev, n, var, 0) == VGLOBAL)
239         return VGLOBAL;
240       var->u.s.info = indexupvalue(fs, n, var);  /* else was LOCAL or UPVAL */
241       var->k = VUPVAL;  /* upvalue in this level */
242       return VUPVAL;
243     }
244   }
245 }

首先看这个函数调用的四个参数含义.fs自不必说,是这条语句所在的chunk对应的FuncState指针,如果在本层chunk中查找不到这个变量,将使用它的prev指针在它的父chunk中继续查找,如果prev指针为空,说明到了全局作用域;n表示的时这个变量的变量名对应的TString指针;var是查找到此变量时用于保存其信息的expdesc指针;base是一个整型值,只有两个可能数据,1表示是在本层进行的查找,0表示是它的子chunk在它这一层做的查找,如果查找到这个变量,那么就是做为UpValue来给子chunk使用的.

有了前面对函数参数的这些分析,再回头看这个函数做的事情就一目了然了:

1. 当传入的fs为NULL的时候,说明此时已经在最外一层了,此时认为这个变量是全局变量.
2. 调用searchvar函数查找该变量,如果其返回值>=0,说明查找到了这个变量,对于这个chunk而言这个变量是局部变量,当base为0的时候,还需要对当前的block做一下标记,这个标记的作用是标记该block中有变量被外部做为UpValue引用了.
3. 当searchvar返回值<0时,说明在这一层没有查找到这个变量,于是使用FuncState结构体的prev指针进入它的父chunk来进行查找,如果查找结果是全局变量就直接返回,否则就是UpValue了,需要对查找一下这个UpValue.indexupvalue这个函数做的事情很简单,就是在该FuncState对应的Proto结构体的Upvalue数组中查找该变量,如果之前没有这个UpValue就新增一个,最后返回这个UpValue在数组中的索引值.

回到singlevar函数中,从singlevaraux函数返回之后,因为变量a是全局变量,所以返回值是VGLOBAL,所以要通过luaK_stringK函数分配一个常量数组的索引,这个索引保存在info中,将作为后面设置setglobal指令Bx的值.

赋值

exprstat函数在调用primaryexp函数得到变量a的expdesc结构体信息之后,会做一个判断,如果前面得到的expdesc类型是一个函数调用(类型为VCALL)时,将做一些处理,由于不属于这里要讨论的情况不在这次中展开讨论;另一种情况则是一个赋值操作了,将会进入assignment函数中进行处理,在此之前会把expdesc结构体的prev指针赋值为NULL,因为这是这个赋值表达式左边的第一个变量,在它之前没有别的变量.

来看assignment函数的实现.

首先会做一个判断,如果下一个token是",",说明赋值表达式的等号左边有多个变量,继续调用primaryexp函数拿到这个变量的expdesc结构体信息,然后再调用assignment函数,只不过此时将nvars参数加1调用--因为此时多了一个参数.可以看到的是,只要有多个参数的赋值操作,那么对assignment函数的递归调用层次和"="左边参数的数量一致.

否则,先读入"="这个token,这时就可以开始处理赋值表达式等号右边的表达式了,这里调用explist1函数同样也是拿到等号右边的expdesc结构体信息.接着比较"="号左边参数数量(nvars)和"="号右边表达式数量(nexps)是否一致,在不一致的情况下需要做一些调整.

这里需要专门讲解一下luaK_exp2nextreg函数,该函数用于将表达式的信息dump到栈中,是一个重要的函数.

luaK_exp2nextreg函数

这个函数主要用于将对应的表达式使用相应的赋值语句赋值到栈中.因此,它做的主要的事情就是以下几件:

  • 根据不同的表达式类型(local/global/upval)使用对应的赋值语句.这里还需要注意的是,有些数据类型可能还需要重定向操作,这一点下面再详细解释.
  • 找到当前栈中下一个可用的栈位置,将第一步的结果赋值给该栈空间.

以这里最简单的情况看,当前表达式是常量10,因此它不是local/global/upval变量,可以在找到栈位置之后使用OP_LOADK指令将该常量赋值到该栈空间.

如果做一些变化,使用变量而不是常量那么就会变得复杂一些.因为global/upval都可能是该函数外部的变量,因此在使用它们之前,都会首先将它们的值赋值到当前函数的栈空间中,而在使用的时候,很多时候并不知道这些变量赋值到栈空间的位置.于是,就需要打一个重定向的标记,检查到该标记的时候,才拿到准确的栈位置.

最后将调用luaK_storevar函数进行具体的赋值操作.由于在这里,变量a是全局变量,因此会走到VGLOBAL这个case中,这个case中的两句代码,第一句将表达式"10"首先分配一个栈地址,通过调用OP_LOADK指令赋值到这个地址中,然后通过调用OP_SETGLOBAL指令将上一步得到的栈地址赋值到全局变量a中.

读取全局变量的值

我们试着在上一条语句的基础上扩展一下,新增一条语句,如下:

a = 10
b = a

这两条语句,除了前面的loadk/setglobal指令之外,还会涉及到另一条指令getglobal,它的格式如下:

OP_GETGLOBAL,/*	A Bx	R(A) := Gbl[Kst(Bx)] */

指令中变量的涵义与前面setglobal的涵义一致,不再多做解释.

第二个表达式将a的值赋值给b的操作,会使用到OP_GETGLOBAL指令,也就是在前面分析luaK_exp2nextreg函数时提到的变量为global的情况,这里就不再多做阐述.而后面的其他过程,跟赋值常量就一样了.

由上面对全局变量的读写操作的分析可知,读写全局变量是一个繁复的过程,中间都需要将全局变量暂存到一个栈空间的变量中,而局部变量就没有这么复杂.我们接下来分析局部变量的读写操作.

局部变量赋值

将前面的代码修改为对局部变量进行赋值操作:

local a = 10

由于解析到了"local"关键字,同时紧跟着的token又不是"function"关键字,即不是函数定义,所以在statement函数中走入了localstat函数中.

这个函数做的事情,其实以"="为界,对两边的表达式做了解析.但是需要注意的是,解析"="号左边的表达式时,也就是这里的"a"变量,是把该变量的信息存放到FuncState结构体的actvar数组中,然后再分析"="号右边的表达式,这个过程和前面一样,最后调用OP_LOADK将常量"10"赋值给函数栈的局部变量.

可是回头看这个过程,有一个疑问,这整个过程并没有涉及到保存函数可用栈信息的FuncState结构体的freereg成员,所以这里的问题在于,局部变量"a"其实占用了一个栈空间的,而在这一整个过程中似乎并没有更新存放可用栈信息的freereg变量.

答案在chunk函数中.每次分析处理完一个chunk的信息,chunk函数会根据nactvar变量,也就是局部变量的数量来进行调整:

1334     ls->fs->freereg = ls->fs->nactvar;  /* free registers */
(lparser.c)

对比以上全局和局部变量的读写分析,可以发现在Lua的指令集中,甚至没有单独的关于局部变量的读写的指令比如loadlocal/getlocal之类的指令,原因在于它们并不需要单独存在,可以依附到任何需要读写局部变量的指令中,也就是说,任何涉及到局部变量的读写操作实际上都可以在一条指令以内完成,对比前面分析的全局变量少了一条指令.

这也就是Lua程序设计中经常提到的一个代码优化原则:尽可能使用局部变量,即使你真的使用的是一个外部的符号,比如库函数其他模块的函数等,也可以先使用一个局部变量将它保存下来以便本模块中使用.

loadnil指令

分析loadnil指令使用的Lua代码,在前面loadk指令的基础上稍作修改:

local a, b = 10

这种情况数据"="号左边表达式数量比右边表达式数量多的情况,将在adjust_assign函数中将多余的表达式赋值为nil:

256 static void adjust_assign (LexState *ls, int nvars, int nexps, expdesc *e) {
257   FuncState *fs = ls->fs;
258   int extra = nvars - nexps;
    /* .... */
267     if (extra > 0) {
268       int reg = fs->freereg;
269       luaK_reserveregs(fs, extra);
270       luaK_nil(fs, reg, extra);
271     }
272
273 }
(lparser.c)

move指令

再来看看局部变量之间的赋值:

local a = 10
local b = a

前面的分析已经知道,一个局部变量,实际是是存放在当前函数的函数栈中的,每个变量对应一个栈位置索引,因此OP_MOVE指令所要做的,就是在两个不同的栈空间位置上进行赋值:

OP_MOVE,/*	A B	R(A) := R(B)				*/

回头来看上面两句Lua代码,第一句使用OP_LOADK指令对局部变量赋值,这个前面已经做过分析,第二句将局部变量a赋值给局部变量b时,将走到discharge2reg函数的这种情况中:

367     case VNONRELOC: {
368       if (reg != e->u.s.info)
369         luaK_codeABC(fs, OP_MOVE, reg, e->u.s.info, 0);
370       break;
371     }

走到VNONRELOC这个case,是因为局部变量的赋值不会涉及到重定向操作,而"reg != e->u.s.info"的判断,是因为两个局部变量所在的函数栈位置不同,所以需要调用OP_MOVE指令从另一个局部变量的栈位置提取数据赋值.

upvalue的读写

所谓的"upvalue",可以理解为介乎于局部变量和全局变量范围的一种变量.从前面查找变量的函数singlevaraux代码可知,查找一个变量只可能有三种结果:

  • 在当前函数层找到该变量,那么就是局部变量.
  • 当查找到该变量时,该层次不存在对应的FuncState结构体,那么认为是全局变量.
  • 排除以上两种情况,就是Upvalue.

我们来看看典型的upvalue的Lua代码示例:

function f()
	local a = 10
	function f1()
		local b = a
	end
end

这段代码中,函数f中有该函数的局部变量a,又有该函数内的局部函数f1,而函数f1内的局部变量b需要拿到变量a的内容,那么此时变量a对于变量b而言就是一个upvalue.

现在可以来看看与upvalue相关的读写指令OP_GETUPVAL/OP_SETUPVAL格式了:

OP_GETUPVAL,/*	A B	R(A) := UpValue[B]		*/
OP_SETUPVAL,/*	A B	UpValue[B] := R(A)		*/

可见在upvalue的操作中,与前面有所区别的就是多了一个UpValue数组的读写操作,于是这里就来看看这个UpValue数组是如何创建和访问的.秘密在前面提到的singlevaraux函数中.

前面提到过,除去局部变量和全局变量的其它情况,才是upvalue,在singlevaraux函数中也是这么处理的:查找一个变量时,当查找到它的时候,既不是局部变量也不是全局变量,那么就会认为是一个upvalue,此时会调用indexupvalue函数来查找upvalue.

这个函数做的事情其实很简单,每个FuncState结构体中,都有一个upvalues数组来存放upvalue信息,所以首先会遍历这个数组,如果查找不到就新增一个upvalue,无论如何这个函数都会返回该upvalue数据在该数组中的索引.后续的读写操作只要针对该upvalue数组即可.