这一节首先从赋值类指令讲起,将会通过最简单的表达式开始,逐渐变化来分析各种赋值类指令的行为。
先来讨论最简单的局部变量赋值,其对应的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中,它主要的工作是以下几个:
- 调用函数expr解析表达式。
- 当解析的表达式列表中还存在其他表达式,即有“,”号分隔的式子时,针对每个表达式继续调用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 }
这里做的就是两个工作:
- 使用类型VKNUM初始化expdesc结构体。
- 将具体的数据也就是这里的10赋值给expdesc结构体中的nval,前面提过,expdesc结构体中的union u的数据,根据不同的类型会存储不同的信息,在VKNUM这个类型下就是用来存储数字的。
好了,现在这个表达式的信息已经存放在expdesc结构体中,需要进一步的根据这个结构体的信息来生成对应的Opcpde。
这个工作由函数luaK_exp2nextreg完成,这是一个非常重要的函数,原因就在于任何根据expdesc结构体需要生成opcode的时候,都需要经过它。它做了如下几个工作:
- 调用luaK_dischargevars函数,根据变量所在的不同作用域(local,global,upvalue)等来决定这个变量是否需要重定向。
- 调用luaK_reserveregs函数,分配可用的函数寄存器空间,得到这个空间对应的寄存器索引,有了空间才能存储变量。
- 调用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的全过程分析。稍微来做一个小结:
- 所有局部变量都会有一个对应的LocVar结构体存储它的变量名信息。
- 解析表达式的结果会存在expdesc结构体中,这个结构体中根据不同的类型,内部使用的联合体存放的数据有不同的意义。
- 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)来进行变量的查找,这个函数的大体流程是:
- 如果变量在当前函数的LocVar结构体数组中找到,那么这个变量就是局部变量,类型为VLOCAL。
- 如果当前函数找不到,那么逐层往上面的block来查找这个变量,如果在某一层查找到了,那么这个变量就是UpValue,类型为VUPVAL。
- 如果最后哪一层都没有查找到,那么这个变量就是全局变量,类型为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类型不同的是:
- 首先生成了一个OP_GETGLOBAL指令,用于获取全局变量的值到当前函数的寄存器中。
- 将类型修改为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到栈中,是一个重要的函数.
这个函数主要用于将对应的表达式使用相应的赋值语句赋值到栈中.因此,它做的主要的事情就是以下几件:
- 根据不同的表达式类型(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指令使用的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)
再来看看局部变量之间的赋值:
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",可以理解为介乎于局部变量和全局变量范围的一种变量.从前面查找变量的函数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数组即可.