第2章 关于条件编译
首先,解释一下,为什么题目叫做:条件编译。其实很简单,现在这一章要分析的是,if、while、repeat、for语句。这些语句有个什么特点呢?那就是,都要有条件判断。根据条件判断的结果,以决定是否执行,该如何执行。我不知道该怎么称呼这样的语句,就一致称其为:条件编译。因为这一章的重点是研究,条件对于中间码的生成造成的影响,以及如何生成中间码以实现这种条件判断。 首先分析if then elseif then else then end句型。 这个很简单,实际上是lua处理的很简单。 当lua遇到了if这个关键字的时候,就开始调用ifstat(),首先,这是进入一个块。这个块这个东西和函数是相同的重要,因为它关系到一个变量的作用域的问题。块由block()函数处理。不过在此之前,要注意一个重要的事情,那就是条件判断语句。lua是怎么处理条件判断的呢? 首先,lua会跳过if关键字,然后,会进入一个十分常用的函数:cond(),这个函数还是比较复杂的,我现在要再看一遍这个函数,还是心有余悸的。 cond()这个函数比较短,我还是贴出来吧:
static int cond (LexState *ls) {
/* cond -> exp */
expdesc v;
expr(ls, &v); /* read condition */
if (v.k == VNIL) v.k = VFALSE; /* `falses' are all equal here */
luaK_goiftrue(ls->fs, &v);
return v.f;
}
第一步,用expr()这个函数读取条件表达式,条件表达式是一个式子,可以是以下几种情况:
-
常数:这种情况比较好办,如果是FALSE或NIL就是条件不通过,其他情况都是条件通过;
-
另一种情况是非常数:也就是说,不是编译期就能判断是否为TRUE或FALSE,要到运行期来判断。
对于这两种情况,分别分析,当然,首先是简单的情况,常数。
当遇到常数的时候,expr()比较happy。因为它处理起来比较简单,也就是通过simpleexp()直接处理。比如,这个常数是个数字100,simpleexp()就会把这个表达式v赋值为:v->k = TK_NUMBER,将v->u.nval = ls->t.seminfo.r,也就是把100给 v->u.nval。如果这个常数是一个字符串,simpleexp()会将这个字符串注册进常量表,并将 v->k = TK_STRING,v->u.s.info赋值为这个字符串在常量表中的位置。如果这个常量是FALSE,NIL,TRUE这3个保留字,simpleexp()就会直接将v->k赋值成 TK_FALSE,TK_NIL,TK_TRUE。 然后,lua会调用luaK_goiftrue()函数。这个函数就是生成判断指令的函数了。 首先,这个函数会把要用到的变量discharge到空闲寄存器上去。也就是生成对应的加载变量的指令。由于现在我们研究的是常量,就不需要这一步,直接跳过。 然后,lua会判断这个常量是FALSE呢还是TRUE呢。如果是TRUE,则将pc = NO_JUMP,这个pc就是说,如果要生成指令的话,就会用这个pc指向这个指令。如果是FASLE,则会生成一条无条件跳转指令:通过luaK_jump()。这条指令会跳到哪呢?下面会有两种情况:
- 后面是elseif,这时,就要跳到这条elseif前面,来做下一次条件判断;
- 后面是else,这时,就跳到这条else前面或后面,因为else本身不生成指令。
怎么实现跳转到这个地方呢?当解析程序解析到if语句开始时,如果要跳转,就要跳过这个if块,到达下一个elseif或else程序块。但是,解析程序怎么知道下一个elseif或else块的开头地址在哪呢?当然,刚开始解析程序是不知道的。只有解析程序把这个if块解析完了,才能知道原来要跳转到这里。所以,之前要把要跳转的那条指令的位置记录下来,到解析完了if块后,再将那条指令跳转的位置修正到此处。lua中就是这么实现的。 具体就是,lua会通过luaK_pathtohere()函数,将要跳转的那条指令保存在fs->jpc里,这个fs->jpc就是指上次要跳转的指令。但是,lua不会立即将跳转指令修改到这里。因为这个elseif或else块中可能没有代码,也就是说,就算跳到这里也是没有的,不会执行任何代码,这时,lua就进行了优化,只有当lua在这个块里生成代码的时候,也就是在luaK_code()函数里,调用dischargejpc(),将保存在fs->jpc里的指令修改为跳到这个地方。 基本的跳转原理就是这样的 了。明白了这些后,再来看复杂条件的跳转是怎么实现的。也就是,在编译期间并不知道条件的结果,到底是真还是假,也就是说,要到运行期间才知道到底要不要跳转。假设我们现在研究这个例子: 例:if a > b then end 这里,就要回到cond()函数里了。看cond()函数是怎么处理a > b这种运行时才能出结果的情况。 首先,cond()会通过调用simpleexp()读取a,然后会读取下一个操作符,发现是>,这个时候,还不能生成代码,因为这里关系到一个操作符优先级的问题,要接着读取后面的操作符,先把优先级高的操作符处理后,才能处理低优先级的操作符,所以,这里就会进行递归读取。在此之前,还有一个问题,那就是,以逻辑操作符连接的式子,比如:AND,OR等,这些操作符的话,其实前面就是一个式子,比如AND,如果是 a AND b,如果a错的话,后面的就不用测试了,如果a 对的话,还要继续测试b,所以,在这里,要为 a生成一个测试及跳转的语句,这里就用到一个函数jumponcond()。 这个函数是怎么实现的呢?这个函数其实是通过调用condjump()实现的。具体来说,很简单,condjump()生成了2个指令,第一条指令是判断指令:OP_TEST或者OP_TESTSET,第二条指令是OP_JUMP,也就是说,如果测试的结果是FALSE,那么,OP_TEST不做任何事情,继续执行,那么就会执行到OP_JUMP指令了,也就是FALSE的时候不执行if块,直接跳转,如果测试成功了呢,OP_TEST会执行跳过下一条指令的操作,也就是跳过了OP_JUMP,继续执行if块。 这就是条件跳转的基本实现原理。
现在回到simpleexp()中来,继续我们的例子:a > b。当读取了>好,会先用luaK_infix()函数进行逻辑AND,OR这两个逻辑运算的代码生成。在这个例子中,没有逻辑运算,于是,lua会继续。这个时候,像前面所说的,要进行操作符优先级的运算,于是,进行递归simpleexp()这个函数,在递归的时候,发现后面只有一个b,那么,就会读取b ,然后,就进入关键的地方了,luaK_posfix()函数。这个函数会根据操作符的不同,生成操作指令,比如我们现在的>,就会通过codecomp()函数,生成OP_LT指令。这个OP_LT指令的执行其实进行了优化,也就是说,如果测试结果为FALSE的话,也就是不通过,理论上是要进入下一条指令跳转的,在执行期间,这两条指令会在一个指令周期里执行,如果测试不通过,直接从下一条跳转指令中取出要跳转的位置,然后跳转到那里。如果测试成功的话,就会跳过下一天条跳转指令,进入if块。 这就是条件跳转代码编译的过程。 ———————————————— 版权声明:本文为CSDN博主「haxixi_keli」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/haxixi_keli/article/details/3224731