Skip to content

Latest commit

 

History

History
478 lines (358 loc) · 28.1 KB

ch04-逻辑关系类指令.md

File metadata and controls

478 lines (358 loc) · 28.1 KB

关系逻辑类指令

相关指令

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指令,完成将比较结果赋值到变量中的操作.

OP_EQ

逻辑类指令

再来看逻辑类指令的处理.前面的关系类指令,其操作之后的结果输出,其实是布尔类型的返回值.而逻辑类指令,其操作结果就不仅限于布尔类型的返回值了,可以是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作为整个表达式的结果返回.