OP_LOADBOOL,/* A B C R(A) := (Bool)B; if (C) pc++ */
OP_JMP,/* sBx pc+=sBx */
OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A) then pc++ */
OP_LT,/* A B C if ((RK(B) < RK(C)) ~= A) then pc++ */
OP_LE,/* A B C if ((RK(B) <= RK(C)) ~= A) then pc++ */
OP_TEST,/* A C if not (R(A) <=> C) then pc++ */
OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */
暂时不展开复杂的讨论,所有的逻辑跳转类指令最后都无非是这样的形式:
如果条件成立跳转到lable1,否则跳转到lable2:
label1:
条件成立的处理
跳转到出口
label2:
条件不成立的处理
跳转到出口
出口:
处理收尾工作
比如,以下的一段C代码:
if (cond)
func1();
else
func2();
func_end();
可以套用上面的模式改写翻译为:
if (cond) {
goto label1;
}
goto lable2;
label1:
func1();
goto label_end;
label2:
func2();
goto label_end;
lable_end:
func_end();
虽然都可以套用这个模式来将条件语句翻译为带有跳转指令的代码,但是有以下几点需要注意:
1. 有一些跳转语句实际上不必要的,比如在label2中的”goto label_end;”这一句实际上不必要的,因为紧跟着的语句就是label_end.
2.这是这里最关键的问题,跳转位置实际上在生成跳转语句的时候经常是不知道的.比如最开始判断cond是否成立来决定跳转位置的时候,实际上label1,label2还并未生成.
对于第一个问题,可以改写代码,从而删去一些多余的语句,改写如下:
if (cond)
goto lable1;
label2:
func2();
goto label_end;
label1:
func1();
lable_end:
func_end();
对于第二个问题,在编译原理的理论中,使用一种称为”回填”(backpatch)的技术来进行处理.它的做法是,生成跳转语句时,将当前还不知道位置,但是都要跳转到同一个位置的语句链接在一起,形成一个空悬跳转语句的链表,在后面找到跳转位置时,再将跳转位置遍历之前的链表填充回去.由于跳转无非就是条件为真和为假两种情况的跳转,所以同一个表达式只有两个跳转链表,一般称为truelist和falselist.
还是以开始的例子来解释这个过程.
if (cond)
// 生成一个跳转语句,此时label1位置未知,因此生成跳转语句的跳转点加入cond的truelist
label2:
func2();
// 生成一个跳转语句,此时label_end位置未知,因此生成跳转语句的跳转点加入cond的falselist
label1:
func1();
lable_end:
func_end();
这里只是最简单的情况,如果有多个elseif的情况处理,那么truelist和falselise就可能不止只有一个元素.
从这里看出,回填技术涉及到几个操作:
1.将当前生成的未知其目的地址的跳转语句加入到某个空悬链表中.
2.以某个位置的数据,回填1中生成的空悬链表的悬空地址.
可以把空悬链表看做是这样的链表:它将一系列空悬的跳转点链接在一起,而它们都将跳转到同一个位置,而当这个位置已知的时候,再将这个地址回填到这些空悬跳转点上完成跳转位置的修正.
另外,跳转地址又分为两种:当前的指令位置,以及其他不是当前指令位置的目的地址,可以把前者看成是后者的特殊情况.
有了前面的理论基础以及对相关指令的了解,现在可以来看看Lua代码中具体的实现了.
首先来看看Lua代码中如何实现跳转链表,即最终跳转位置一样的指令是如何链接在一起的.
在OP_JMP指令中,sBx参数是做为跳转目的地址的偏移量存在的,Lua在实现时,是将一系列将会跳转到同一个地址的OP_JMP指令的sBx参数链接在一起的,比如A,B,C三个OP_JMP指令最后都是跳转到同一个目的地址,而生成这几条指令的时候最终的目的地址并不知道,那么会首先将A的跳转地址设置为B指令的偏移量,同理B指令的跳转地址设置为C指令的偏移量,而在这个跳转链表的最后一个元素C指令,其跳转地址设置为NO_JUMP(-1),表示它是链表的最后一个元素.
将一个新的跳转位置加入空悬跳转链表的操作在函数:
(lcode.c)
185 void luaK_concat (FuncState *fs, int *l1, int l2) {
186 if (l2 == NO_JUMP) return;
187 else if (*l1 == NO_JUMP)
188 *l1 = l2;
189 else {
190 int list = *l1;
191 int next;
192 while ((next = getjump(fs, list)) != NO_JUMP) /* find last element */
193 list = next;
194 fixjump(fs, list, l2);
195 }
196 }
这里的参数l1是空悬链表的第一个指令位置,l2是待加入该链表的指令位置,其中处理了三种情况:
1. 如果l2是NO_JUMP,直接返回,因为这个位置存储的指令不是一个跳转指令;
2. 如果l1是NO_JUMP,说明这个跳转链表为空当前没有空悬的跳转指令在该链表中,直接赋值为l2;
3. 来到最后一种情况说明l1现在是一个非空的跳转链表,首先遍历这个链表到最后一个元素,其判定标准是跳转位置为NO_JUMP时表示是跳转链表的最后一个元素,然后调用fixjump函数将最后一个元素的跳转位置设置为l2,这样l2就添加到了该跳转链表中.
可以看到,这个跳转链表的实现并不像经典的链表实现那样,有一个类似next的指针指向下一个元素,而是利用了跳转指令中的跳转地址这一个参数,来存储链表下一个元素的值. 同时在上面还看到了两个函数getjump和fixjump.getjump函数根据指令得到sBx参数值,但是由于sBx是相对位置,所以还需要转换成绝对位置.fixjump则是计算两个指令之间的偏移量做为跳转指令的sBx参数值设置进去.
有了前面将一个新指令添加到跳转链表的基础,回填跳转地址的流程就很清晰明了,做的事情也是遍历整个链表,修改每个指令的sBx参数值:
(lcode.c)
150 static void patchlistaux (FuncState *fs, int list, int vtarget, int reg,
151 int dtarget) {
152 while (list != NO_JUMP) {
153 int next = getjump(fs, list);
154 if (patchtestreg(fs, list, reg))
155 fixjump(fs, list, vtarget);
156 else
157 fixjump(fs, list, dtarget); /* jump to default target */
158 list = next;
159 }
160 }
这里除了前面提到的getjump和fixjump函数之外,还有对另一个函数patchtestreg的调用,不过目前暂时不对该函数做解释,只需要知道patchlistaux函数中做的事情就是遍历一个跳转链表的所有元素,调用fixjump函数将跳转地址回填到链表中的每个指令中.
上面明白了回填地址时的操作,接着来看用于回填地址的相关数据.FuncState结构体有一个名为jpc的成员,它将需要回填为下一个待生成指令地址的跳转指令链接到一起来,这个操作是在函数中进行的:
(lcode.c)
180 void luaK_patchtohere (FuncState *fs, int list) {
181 luaK_getlabel(fs);
182 luaK_concat(fs, &fs->jpc, list);
183 }
luaK_code函数是每次新生成一个指令最终会调用的函数,在这里会调用函数dischargejpc中遍历jpc链表,使用当前的pc指针进行回填操作:
(lcode.c)
164 static void dischargejpc (FuncState *fs) {
165 patchlistaux(fs, fs->jpc, fs->pc, NO_REG, fs->pc);
166 fs->jpc = NO_JUMP;
167 }
注意到在luaK_code函数的最后,返回新生成指令的pc指针时,都会将pc指针做一个"++"操作,因此下一次再调用luaK_code函数走到dischargejpc函数时的pc指针自然都是下一个待生成指令的pc指针.
关于jpc,还需要注意的是函数luaK_jump中的操作:
(lcode.c)
59 int luaK_jump (FuncState *fs) {
60 int jpc = fs->jpc; /* save list of jumps to here */
61 int j;
62 fs->jpc = NO_JUMP;
63 j = luaK_codeAsBx(fs, OP_JMP, 0, NO_JUMP);
64 luaK_concat(fs, &j, jpc); /* keep them on hold */
65 return j;
66 }
在这里,首先预存了当前的jpc,然后将当前FuncState的jpc指针置为NO_JUMP,再调用luaK_codeAsBx生成OP_JMP指令,最后将前面预存的jpc指针加入到新生成的OP_JMP指令的跳转位置中.这里之所以这么做,是因为如果当前即将生成的指令是OP_JMP跳转指令,如果按照这个新生成的跳转指令进行回填操作,那么jpc链表中悬空的跳转指令将会首先跳转到这个跳转指令上,再次从这个跳转指令上再跳转到最终的目的地址,实际上做了两次跳转.因此在这里,生成跳转指令之前,首先将FuncState结构体的jpc指针置为无效的跳转地址,这样在生成跳转指令时调用dischargejpc时就不会将下一个pc指令的地址遍历jpc链表进行回填,因为此时的jpc链表已经无效.而是在生成跳转指令后,再将之前保存的jpc链表加入到这个跳转指令的链表中,这样再最终拿到目的地址时一次性进行回填操作,就不会在执行时做两次跳转操作了.可以看到,这里对跳转指令做了一个优化,将原来需要进行连续两次跳转的操作,优化为只需要跳转一次就能到最终的目的地址.
jpc链表中保存的是将要跳转到下一个待生成指令的跳转指令链表,而如果是要跳转到当前指令,那么其实需要的就是当前的pc指针,这是在函数luaK_getlabel中返回的:
(lcode.c)
94 int luaK_getlabel (FuncState *fs) {
95 fs->lasttarget = fs->pc;
96 return fs->pc;
97 }
现在可以来看看这一节需要涉及的相关指令了.关系类指令OP_EQ,OP_LT,OP_LE分别用在生成等于,大于以及大于等于关系指令,而不等于,小于以及小于等于可以看做是这三个指令操作的值取反的情况,所以这里并没有后面这三种关系对应的指令,后面的三种关系可以由前三种指令做变化来生成.这三种指令的特点是,它们是将栈中的两个值进行对比,根据结果做下一步的操作.因此,一个关系类指令,后面一定跟随着一个跳转指令,以及两个OP_LOADBOOL指令,其中的OP_LOADBOOL指令用于加载前面的比较结果,跳转指令用于根据比较结果来选择是跳转到哪一条OP_LOADBOOL指令.OP_LOADBOOL指令的格式很简单:
OP_LOADBOOL,/* A B C R(A) := (Bool)B; if (C) pc++ */
它的作用是,向R(A)中赋值布尔类型的参数B的值,再根据参数C的值决定是否将pc指针加1,也就是是否跳过下一条指令.
我们来看一个例子,由于三条关系类指令的处理类似,这里只分析其中的一个即可,其他两个的原理类似:
local a,b,c
c = a == b
使用ChunkSpy分析生成的指令:
.function 0 0 2 3
.local "a" ; 0
.local "b" ; 1
.local "c" ; 2
[1] eq 1 0 1 ; to [3] if false
[2] jmp 1 ; to [4]
[3] loadbool 2 0 1 ; false, to [5]
[4] loadbool 2 1 0 ; true
[5] return 0 1
逐条分析一下生成的指令:
[1]:比较局部变量a(R(0))以及b(R(1))的值是否相等,该结果的布尔值与1(参数A)相比,如果不相等就将PC指针加1,也就是跳转到指令[3].
[2]:OP_JMP指令的跳转偏移量是1,也就是跳过前面的一条指令到指令[4].
[3]:局部变量C(R(2))赋值为0(参数B),因为参数C是1,于是PC指针加1,跳转到指令[5].
[4]:局部变量C(R(2))赋值为1(参数B),因为参数C是0,于是PC指针加0,也就是不进行跳转操作.
[5]:可以认为是这这个OP_EQ指令的逻辑意义上的下一条指令,前面的跳转指令会根据这个指令的地址进行回填.
从上面的分析可以看到,真正将关系比较的结果进行赋值的操作,实际上是在两条OP_LOADBOOL指令中进行的,OP_EQ所做的,是进行实际的比较操作,同时根据比较的结果进行不同的跳转,以此来选择对应情况的OP_LOADBOOL指令,完成将比较结果赋值到变量中的操作.
再来看逻辑类指令的处理.前面的关系类指令,其操作之后的结果输出,其实是布尔类型的返回值.而逻辑类指令,其操作结果就不仅限于布尔类型的返回值了,可以是Lua中任意类型的值,因此它不会有OP_LOADBOOL指令来加载它的指令结果到变量中的操作,但是在指令中有赋值结果到变量中的操作.同时,由于逻辑类指令使用判断某个变量的值是不是为False的情况来做其他的操作,而不是像关系类指令那样是两个变量之间的比较,所以逻辑类指令中进行比较的是一个变量以及一个参数值.而两类指令相同的是,都需要紧跟OP_JMP跳转指令来针对不同的结果来做处理.来看看具体的格式:
OP_TEST,/* A C if not (R(A) <=> C) then pc++ */
OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */
逻辑类指令,首先会把待比较的变量的值,转换为对应的布尔类型值.比如如果一个变量的值是NIL(或者没有定义),或者本身就是布尔类型的False,都会被认为是False.可以看到,OP_TEST与OP_TESTSET指令之间,除了进行比较的操作参数不同(OP_TEST是对比R(A)和参数C,OP_TESTSET是对比R(B)和C)之外,可以将OP_TEST指令看做是OP_TESTSET的特殊情况,OP_TESTSET指令比OP_TEST指令多了一个将R(B)赋值给R(A)的操作,而OP_TEST可以看做也有此处理,只不过是R(B)赋值给R(B),也就是没有赋值操作的情况.我们这里首先分析OP_TESTSET指令,后面再分析两者在处理上的差异在哪里.
这里使用的测试指令是:
local a,b,c
c = a and b
ChunkSpy指令中看到的输出是:
.function 0 0 2 3
.local "a" ; 0
.local "b" ; 1
.local "c" ; 2
[1] testset 2 0 0 ; to [3] if true
[2] jmp 1 ; to [4]
[3] move 2 1
[4] return 0 1
逐条分析指令:
[1]:比较局部变量a(R(B))与参数C的值(0),如果两者相等,即说变量a的值相当于布尔类型的False,那么将变量a的值赋值给变量c(R(A)),否则PC指针加1,跳转到指令[3].
[2]:如果走到这个跳转指令,说明前面变量a的值为False,此时已经将变量a的值赋值给变量c了,于是跳转到指令[4].
[3]:如果走到这个跳转指令,说明前面变量a的值不为False,那么需要将变量b的值赋值给变量c.
[4]:可以看做是这个关系指令逻辑意义上的下一条指令,前面的跳转指令会根据这个指令的地址进行回填.
可以看到,这里的指令走向,取决于变量a的值,指令[2]-[4]是变量a为False情况下的走向,也就是这个表达式的truelist;指令[3]-[4]是变量a为True情况下的走向,也就是这个表达式的truelist.
有了前面的准备,我们已经可以进入代码中看看这些指令是如何生成的了,由于关系类指令相比逻辑类指令相对简单,前面的关系类指令不具体进行分析,仅分析后面的逻辑类指令.
对"and"操作符的处理,最终会调用到函数luaK_goiftrue中,该函数的第二个参数expdesc结构体指针对应的是"and"操作符的第一个操作表达式,该函数命名为luaK_goiftrue的意思,也就是该表达式在true情况下的走向.对应的,操作符"or"对应的是函数luaK_goiffalse.我们来看看这个函数:
(lcode.c)
539 void luaK_goiftrue (FuncState *fs, expdesc *e) {
540 int pc; /* pc of last jump */
541 luaK_dischargevars(fs, e);
542 switch (e->k) {
543 case VK: case VKNUM: case VTRUE: {
544 pc = NO_JUMP; /* always true; do nothing */
545 break;
546 }
547 case VFALSE: {
548 pc = luaK_jump(fs); /* always jump */
549 break;
550 }
551 case VJMP: {
552 invertjump(fs, e);
553 pc = e->u.s.info;
554 break;
555 }
556 default: {
557 pc = jumponcond(fs, e, 0);
558 break;
559 }
560 }
561 luaK_concat(fs, &e->f, pc); /* insert last jump in `f' list */
562 luaK_patchtohere(fs, e->t);
563 e->t = NO_JUMP;
564 }
541: 首先调用函数将传入的表达式解析出来.
542-560:根据解析出来的表达式类型做不同的处理.分为以下几种情况: 543-546:当表达式是常量(VK),VKNUM(数字)以及VTRUE(布尔类型的True)时,这些情况下该表达式都认为是True的,并不需要增加一个跳转指令跳过下一条指令; 547-550:反之如果是VFALSE,那么就需要生成一个跳转指令了. 551-555:如果是VJMP,说明表达式v是一个逻辑类指令,这个时候需要将它的跳转条件进行颠倒操作.比如前面的表达式如果是比较变量A是否等于变量B,那么在这里会被改写成变量A是否不等于变量B. 556-559:最后一种是默认情况,此时需要进入jumponcond函数中生成针对表达式v为False情况的OP_TESTSET指令,注意这里传入jumponcond函数中的cond参数是0,也就是生成的是表达式为False情况下的指令.
561:前面根据表达式的不同类型生成跳转指令,该指令的地址返回在局部变量pc中.可以看到,pc可能有两种情况,一种为NO_JUMP,这种情况下是表达式恒为True的情况,比如情况1,其他情况最终都会生成跳转指令,而这些跳转都发生在表达式v为False的情况下.因此,这里将返回的pc变量加入到表达式的falselist中.
562:调用luaK_patchtohere函数将表达式的truelist加入到jpc跳转链表中,前面已经分析过了,这将在生成下一条指令的时候将下一条指令的将下一条指令的pc遍历jpc链表进行回填操作.换言之,表达式e为true的情况将跳转到前面生成的跳转指令的下一条指令.
(lcode.c)
391 static void exp2reg (FuncState *fs, expdesc *e, int reg) {
392 discharge2reg(fs, e, reg);
393 if (e->k == VJMP)
394 luaK_concat(fs, &e->t, e->u.s.info); /* put this jump in `t' list */
395 if (hasjumps(e)) {
396 int final; /* position after whole expression */
397 int p_f = NO_JUMP; /* position of an eventual LOAD false */
398 int p_t = NO_JUMP; /* position of an eventual LOAD true */
399 if (need_value(fs, e->t) || need_value(fs, e->f)) {
400 int fj = (e->k == VJMP) ? NO_JUMP : luaK_jump(fs);
401 p_f = code_label(fs, reg, 0, 1);
402 p_t = code_label(fs, reg, 1, 0);
403 luaK_patchtohere(fs, fj);
404 }
405 final = luaK_getlabel(fs);
406 patchlistaux(fs, e->f, final, reg, p_f);
407 patchlistaux(fs, e->t, final, reg, p_t);
408 }
409 e->f = e->t = NO_JUMP;
410 e->u.s.info = reg;
411 e->k = VNONRELOC;
412 }
最终会调用exp2reg函数进行回填操作,这里只看与回填操作相关的核心代码.
392:将表达式e值解析出来,赋值到寄存器reg中.
395:hasjumps函数判断表达式e的truelist和falselist中只要有一个链表有元素,那么就表示有回填操作,在这种情况下会进入下面的处理.
396-398:定义了三个局部变量,其中final表示的这里这个表达式的下一个指令地址.
399:分别调用need_value遍历表达式e的truelist以及falselist,只要链表中其中一个元素不是紧跟在OP_TESTSET指令后面的跳转指令,该函数就返回1.原因在于,除了OP_TESTSET指令外,逻辑类指令OP_EQ等,由我们前面的分析可知,都需要生成OP_LOADBOOL指令加载返回的布尔类型值来做处理.当然,对应这里分析的Lua代码,只涉及到逻辑类指令,不会走到这个条件判断中,可以回头看前面的关系类指令来分析这部分代码.
400-403:fj将根据表达式e的类型来判断是否生成跳转指令,在403行中将调用luaK_patchtohere函数将fj加入到jpc链表中.p_f和p_t保存的是生成的OP_LOADBOOL指令的地址,也就是根据表达式e的结果来做不同的跳转处理,这一点前面分析关系指令的时候已经做过分析.
405:得到下一个指令的pc指针,返回到变量final中.
406-407:调用函数patchlistaux对表达式e的truelist和falselist进行回填操作.前面曾经对patchlistaux进行过简单的分析,但是还有细节没有交代清楚,在这里终于可以展开来讨论了,为了方便再次将patchlistaux函数的代码列举一下:
(lcode.c)
150 static void patchlistaux (FuncState *fs, int list, int vtarget, int reg,
151 int dtarget) {
152 while (list != NO_JUMP) {
153 int next = getjump(fs, list);
154 if (patchtestreg(fs, list, reg))
155 fixjump(fs, list, vtarget);
156 else
157 fixjump(fs, list, dtarget); /* jump to default target */
158 list = next;
159 }
160 }
这里首先需要关注的函数是patchtestreg,如果传入的跳转指令是紧跟在OP_TESTSET指令的,那么就返回1,这里暂时不展开讨论这个函数,仅需要知道这个结果就好.
继续回到函数patchlistaux中,可以看到虽然这个函数做的事情是遍历整个跳转链表,调用fixjump函数回填跳转指令地址,但是这里会根据patchtestreg来区分回填进去的vtarget还是dtarget.我们回头看一下,这里的参数vtarget对应的是exp2reg函数中的final,而dtarget对应的是exp2reg中的p_f/p_t.
回头整理一下,这几个变量值根据跳转指令是否紧跟在OP_TESTSET分为两种情况:
1.need_value返回1,说明truelist或者falselist中有一个跳转指令不是紧跟在OP_TESTSET指令,这种情况下存在需要加载表达式的比较结果的情况,因此p_t/p_f都不会为OP_JUMP,而是有对应的OP_LOADBOOL指令地址.
2.need_value函数针对truelist以及falselist都返回0,表示跳转链表中的跳转指令都是紧跟在OP_TESTSET指令之后,此时表达式不返回布尔型结果,因此不需要补充OP_LOADBOOL指令.
因此,patchlistaux函数中的vtarget意指的是"value target",表示此时所需的非布尔类型值已经在reg寄存器中,此时只需要使用final,也就是表达式的下一个指令地址对跳转地址进行回填;而dtarget意指的是"default target",表示此时表达式需要的是布尔类型值,此时需要使用falselist或者truelist回填跳转地址.
我们接下来来看看逻辑操作中何时生成的是OP_TEST指令而不是OP_TESTSET指令.答案就在前面没有展开分析的patchtestreg函数中:
(lcode.c)
132 static int patchtestreg (FuncState *fs, int node, int reg) {
133 Instruction *i = getjumpcontrol(fs, node);
134 if (GET_OPCODE(*i) != OP_TESTSET)
135 return 0; /* cannot patch other instructions */
136 if (reg != NO_REG && reg != GETARG_B(*i))
137 SETARG_A(*i, reg);
138 else /* no register to put value or register already has the value */
139 *i = CREATE_ABC(OP_TEST, GETARG_B(*i), 0, GETARG_C(*i));
140
141 return 1;
142 }
从这里可以看到,在跳转指令不是紧跟在OP_TESTSET指令后面的情况下,patchtestreg返回0,于是在patchlistaux函数中使用dtarget进行回填操作,这些前面已经阐述过.这里的重点是OP_TESTSET情况下的处理.
这里的reg是需要赋值的目的寄存器地址,也就是OP_TESTSET指令中的参数A,当这个值有效同时不等于参数B时,直接使用这个值赋值给OP_TESTSET指令的参数A. 否则就是没有寄存器进行赋值,或者寄存器中已经存在值(参数A与参数B相等的情况下),此时将原先的OP_TESTSET指令修改为OP_TEST指令.
什么情况下不需要进行赋值呢?如果把前面的Lua代码修改为"a = a and b",这种情况下a的值可能是a或者b,但是由于左边的a与右边的a是同一个变量,此时并不需要将a再赋值给a,也就是前面的参数A与参数B是同一个值的情况,可以自己验证一下这种情况.
讲到这里,其实前面还省略了一个地方,就是Lua代码中对表达式的处理,我们这里回头看一看,对应的测试代码仍然是前面分析逻辑类指令的代码.解析一个表达式的入口函数是subexpr:
(lparser.c)
810 static const struct {
811 lu_byte left; /* left priority for each binary operator */
812 lu_byte right; /* right priority */
813 } priority[] = { /* ORDER OPR */
814 {6, 6}, {6, 6}, {7, 7}, {7, 7}, {7, 7}, /* `+' `-' `/' `%' */
815 {10, 9}, {5, 4}, /* power and concat (right associative) */
816 {3, 3}, {3, 3}, /* equality and inequality */
817 {3, 3}, {3, 3}, {3, 3}, {3, 3}, /* order */
818 {2, 2}, {1, 1} /* logical (and/or) */
819 };
820
821 #define UNARY_PRIORITY 8 /* priority for unary operators */
822
823
824 /*
825 ** subexpr -> (simpleexp | unop subexpr) { binop subexpr }
826 ** where `binop' is any binary operator with a priority higher than `limit'
827 */
828 static BinOpr subexpr (LexState *ls, expdesc *v, unsigned int limit) {
829 BinOpr op;
830 UnOpr uop;
831 enterlevel(ls);
832 uop = getunopr(ls->t.token);
833 if (uop != OPR_NOUNOPR) {
834 luaX_next(ls);
835 subexpr(ls, v, UNARY_PRIORITY);
836 luaK_prefix(ls->fs, uop, v);
837 }
838 else simpleexp(ls, v);
839 /* expand while operators have priorities higher than `limit' */
840 op = getbinopr(ls->t.token);
841 while (op != OPR_NOBINOPR && priority[op].left > limit) {
842 expdesc v2;
843 BinOpr nextop;
844 luaX_next(ls);
845 luaK_infix(ls->fs, op, v);
846 /* read sub-expression with higher priority */
847 nextop = subexpr(ls, &v2, priority[op].right);
848 luaK_posfix(ls->fs, op, v, &v2);
849 op = nextop;
850 }
851 leavelevel(ls);
852 return op; /* return first untreated operator */
853 }
在注释中可以看到,解析一个表达式可能是一个递归的过程,会递归调用函数subexpr,该函数中的参数limit表示的是当前解析到的表达式操作符的右结合的优先级.这些优先级都定义在数组priority中.
在计算整个表达式的时候,如果分析到的操作符是二元操作符,那么会根据每个操作符的左右优先级来进行计算.如果当前分析到的操作符的左优先级大于上一个操作符的右优先级,就会优先计算当前的操作,而如果一元操作符,可以看到使用了宏UNARY_PRIORITY定义所有的一元操作符的优先级是8.
比如"1 * 2 + 3",解析到操作符""之后,根据操作""的右优先级7递归调用subexpr函数,在这里解析到操作符"+",它的左优先级是6,因此不会进入while循环中首先计算操作符"+"的表达式,而是退出递归调用的subexpr函数首先计算"1 * 2",再根据结果计算"2 + 3".
可以看到,有些操作符的左右结合优先级并不一样,比如OPR_POW和OPR_CONCAT的左右优先级就不一样,左边比右边高,这说明这两个操作是右结合的,换言之如果有连着两个同样的OPR_POW操作,会优先计算右边的式子.
现在可以来整体看一下这个函数.它做的其实是几个动作:
1.如果解析到一元操作符,会以一元操作符的优先级递归调用subexpr.
2.真正解析一个表达式的操作是在函数simpleexp中进行的.
3.解析完表达式之后,调用getbinopr解析二元操作符,此后根据前面分析过的操作符优先级来判断计算的顺序.
需要注意的是,这里分别有三个函数luaK_prefix,luaK_infix,luaK_posfix,分别用在解析完一元操作符之后,解析完二元操作符之后以及计算了一个二元操作的表达式之后.
以前面的关系类指令的代码来看,由于不是操作符"and"不是一元操作符,所以没有调用到luaK_prefix函数,不在这里分析.当解析了"a"以及操作符and之后,调用函数luaK_infix,此时的expdesc对应的时已经解析过的变量a,这里面针对操作符and进入函数luaK_goiftrue中,于是就是我们前面分析的那样,在这里将紧跟着的跳转指令(如果有的话)添加到expdesc的falselist,把truelist指向下一条指令,也就是说当变量a为true的时候继续执行下一条指令,否则跳转.
而走到luaK_posfix函数时,它的处理则是:
(lcode.c)
746 void luaK_posfix (FuncState *fs, BinOpr op, expdesc *e1, expdesc *e2) {
747 switch (op) {
748 case OPR_AND: {
749 lua_assert(e1->t == NO_JUMP); /* list must be closed */
750 luaK_dischargevars(fs, e2);
751 luaK_concat(fs, &e2->f, e1->f);
752 *e1 = *e2;
753 break;
754 }
}
这里的e1对应的是变量a,e2对应的是变量b,在这里将e1的falselist加入到e2的falselist中,因为两者如果同为false的情况下跳转到的地方应是一样的.再将e2赋值给e1,最后将e1作为整个表达式的结果返回.