在上一篇“lua2.1语法解析过程(1)”中,我们已经了解了lua的词法和语法解析过程中的关键数据结构YYSTYPE,并且决定采用自下向上、以小见大的方方法来逐步分析,本文中重点关注lua2.1对“表达式”的解析过程(令YYSTYPE类型的全局量yylval):
exp -> number:
一个数据常量
词法分析:yylval.vFloat保存number对应的数值,向语法分析器返回NUMBER。
词法分析:收到NUMBER,生成指令PUSH yylval.vFloat(事实上并非如此简单,这里只是简化了); $$ = 1;
exp -> string:
一个字符串常量
词法分析:将字符串保存到全局符号表中,为其分配一个索引I,I全局唯一标识这个字符串常量;yylval.vWord = I,向语法分析器返回STRING。
语法分析:收到STRING,生成指令PUSHSTRING yylval.vWord; $$ = 1;
exp -> nil:
一个nil对象
词法分析:返回NIL
语法分析:收到NIL,生成指令PUSHNIL; $$ = 1;
expr1 -> expr {if ($1 == 0) code_byte(1);}:
词法分析:当expr在语法分析过程中返回0($$ = 0)的时候将在指令字节串中添加一个字节,值为1。
注:什么时候才会执行$$ = 0?这个问题需要对每个产生式都进行分析产能总结的出来。
expr -> expr1 EQ expr1:
词法分析:遇到“==”将返回EQ
语法分析:假设有这么一条语句:"1 == 2",那么产生指令的顺序是:PUSH1 PUSH2 EQOP; $$ = 1。
expr -> expr1 '<' expr1:
语法分析:假设有这么一条语句:"1 < 2",那么产生指令的顺序是:PUSH1 PUSH2 LTOP; $$ = 1。
expr -> expr1 '>' expr1:
语法分析:假设有这么一条语句:"1 > 2",那么产生指令的顺序是:PUSH1 PUSH2 GTOP; $$ = 1。
expr -> expr1 NE expr1:
词法分析:遇到“~=”将返回EQ
语法分析:假设有这么一条语句:"1 ~= 2",那么产生指令的顺序是:PUSH1 PUSH2 EQOP NOTOP; $$ = 1。
expr -> expr1 LE expr1:
词法分析:遇到“<=”返回LE
语法分析:假设有这么一条语句:"1 <= 2",那么产生指令的顺序是:PUSH1 PUSH2 LEOP; $$ = 1。
expr -> expr1 GE expr1:
词法分析:遇到“>=”返回GE
语法分析:假设有这么一条语句:"1 >= 2",那么产生指令的顺序是:PUSH1 PUSH2 GEOP; $$ = 1。
expr-> expr1 '+' expr1:
词法分析:
语法分析:假设有这么一条语句:"1 + 2",那么产生的指令的顺序是:PUSH1 PUSH2 ADDOP; $$ = 1。
expr-> expr1 '-' expr1:
词法分析:
语法分析:假设有这么一条语句:"1 - 2",那么产生的指令的顺序是:PUSH1 PUSH2 SUBOP; $$ = 1。
expr-> expr1 '*' expr1:
词法分析:
语法分析:假设有这么一条语句:"1 * 2",那么产生的指令的顺序是:PUSH1 PUSH2 MULTOP; $$ = 1。
expr-> expr1 '/' expr1:
词法分析:
语法分析:假设有这么一条语句:"1 / 2",那么产生的指令的顺序是:PUSH1 PUSH2 DIVTOP; $$ = 1。
expr-> expr1 '^' expr1:
词法分析:
语法分析:假设有这么一条语句:"1 ^ 2",那么产生的指令的顺序是:PUSH1 PUSH2 POWOP; $$ = 1。
expr-> expr1 '..' expr1:
词法分析:
语法分析:假设有这么一条语句:"1 .. 2",那么产生的指令的顺序是:PUSH1 PUSH2 CONCOP; $$ = 1。
expr->table:这个产生式比较复杂,下面尝试分析。
我们先看table的产生式:
table-> { fieldlist }
lua2.1在开始分析这个产生式之前首先产生指令:CREATEARRAY 0,在“lua2.1指令笔记”中有说明:创建一个关联数据,并将它放到栈顶。其中0表示他的大小为0,注意,在分析fieldlist之后这个0会变成一个合适的值。这个0只是在最初起到占位的作用。
显然分析fieldlist才是主要分析工作:
在分析之前我们先猜测这个产生式究竟会产生什么样的指令呢?对于一个数组的初始化语句来说,对每个key赋值是最一般的情况,所以我想这个产生式生成的指令一般是以“PUSH系列”+STOREINDEX指令为主,下面接着分析以验证我们的猜测。
显然分析fieldlist才是主要分析工作:
在分析之前我们先猜测这个产生式究竟会产生什么样的指令呢?对于一个数组的初始化语句来说,对每个key赋值是最一般的情况,所以我想这个产生式生成的指令一般是以“PUSH系列”+STOREINDEX指令为主,下面接着分析以验证我们的猜测。
& lt;em>fieldlist -> null
| lfieldlist1 lastcomma
| ffieldlist1 lastcomma
| lfieldlist1 ; ffieldlist1 lastcomma
lastcomma -> null | ,
ffieldlist1 -> ffield
| ffieldlist1 , ffield
ffield -> name = expr1
lfieldlist1 -> expr1
| lfieldlist1 , expr1
上面的产生式是从文章“lua2.1文法”中取出的,仔细分析我们知道table初始化语句有3种形式:
第一种:{1, 2, 3}
第二种:{a1 = 1, a2 = 2, a3 = 3}
第三种:{1, 2, 3; a1 = 1, a2 = 2, a3 = 3}
其中第一种中fieldlist全部由lfieldlist组成,第二种全部由ffieldlist组成,第三种由两种混合而成,需要注意的是,lfieldlist和
ffieldlist并不相互使用。所以语句a = {1, 2, 3; a1 = 1, a2 = 2, a3 = 3; 4, 5, 6}或a = {a1 = 1, a2 = 2, a3 = 3;1, 2, 3;}都是错误的。
需要说明的是fieldlist对应的三条产生的右边式子在分析的过程中对$$解析的意义是“域的数量”,这个值会被用于修改前面我们说的"CTREATEARRAY 0"中的0值。
对于第一种情况,产生的指令是:CREATEARRAY 3 PUSH1 PUSH2 PUSHBYTE 3 STORELIST0 3,这里STORELIST0指令可能会变成STORELIST,那么什么时候才会变成STORELIST呢,在lua2.1中当这种情况下表的域的数量大于40(可修改,但需要重新编译)的时候会使用SOTRELIST指令,这个主要是为了减少指令浪费的空间而设定的(STORELIST m n指令中m和n的都是一个字节,所以在lua2.1中表的初始化的时候最大的域数为256 * 40 + 39)。
对于第二种情况,产生的指令是:CREATEARRAY 3 PUSH1 PUSH2 PUSHBYTE 3 STORERECORE 3 index(a1) index(a2) index(a3)。这里需要注意的是但域数大于40的时候,会有多条STORERECODE指令产生,目的同样是为了减少指令可能浪费的空间。
对于第三中情况,是第一种和第二种情况的结合,这里就不在说明。
分析下来我们发现结果和最初的假设并不相符,lua中为节省指令字符串的大小而使用了STORERECORD和STORELIST系列指令,
用于“批赋值”。
分析table这个产生式的时候显然耗费了我们大量的精力,完成之后分析工作还远没有结束,相反,后面的产生式将更加具有挑战性。
expr->varexp:
在分析之前我们先对这个varexp进行猜测,显然见文知意,这个是描述lua中的变量的非终结符,它的一个工作应该是在全局
对象表中注册这个变量,另外一个工作是将这些变量对象依次放置到栈顶(为什么要放到栈上呢?因为lua2.1是基于栈的虚拟机,绝大
部分操作都是在栈上操作,我们不管是对变量是赋值还是取值,都它所对应的lua对象都应该在栈上)。
涉及的指令应该有PUSHGLOBAL和PUSHLOCAL。
varexp -> var
var -> singlevar
| varexp [ expr1 ]
| varexp . name
singlevar -> name
上面的文法摘自文章“lua2.1文法”
varexp -> var:在分析完非终结符var后,将所有分析过程中遇到的变量(注意:只能可能一个)放到栈顶。
singlevar -> name:这个产生式很重要,因为在在这里辨别一个变量是全局变量和局部变量,并且如果是全局变量的时候,$$为全局变量
在全局对象表中的索引,如果$$为负值,它返回的是一个与这个变量在栈上的偏移值有关的负数。
var -> singlevar:将singlevar返回的值直接返回给var。
var -> varexp [ expr1 ]:这个产生式分析过程是这样:将varexp的结果A放到栈上,将expr1的分析结果B放到栈上,产生指令PUSHINDEX,这个指令在实际的运行过程中是PUSHINDEX A B,也就是取A的所以B对应的值。
varexp . name和varexp [ expr1 ]类似,这里就不做分析。
经过分析,我们发现分析结果和分析前的猜测大致相同,不过多了一个指令PUSHINDEX。
expr -> not expr1:
词法分析:遇到not返回NOT
语法分析:expr1的分析结果一定是在栈顶,分析之后将指令NOTOP入栈。
稍事休息之后我们接着来个复杂的,函数调用。
在分析之前我们再次回顾一下$$的作用,$$是当前分析过程向它的上一层的分析过程报告信息的重要储存结构。比如产生式A->B,
那么B会通过$$向A->B汇报结果。
expr->functioncall:
这个产生式分析过程中将$$置为0,且先看$$=0之后会产生什么效果,分析代码发现,当$$=0的时候,将在该产生式生成的指令字节
串后面添加一个字节且值为0,目前暂不深究添加这个0的作用。
functioncall -> funcvalue funcParams
当将funcvalue和funcParams分析结束之后,会紧跟着添加指令CALLFUNC 和参数数量和返回值数量。先不管返回值数量,我
们先看参数数量是如何计算出来的?如果说是从分析funcParams得到的话,那么你就只对了一半,注意funcvalue也有可能贡献一个,
当a:func的时候就会贡献一个参数。
funcvalue没什么好分析的其实就是生成将“func(...)”中的func对应的对象放置到栈顶的指令,而重头戏在于funcParams:
funcParams -> ( exprlist ) | table:
注:这个产生式将$$设定为分析过程中得到的参数的数量。
table自然就不需要再多分析,不过这里注意的是当遇到table的时候funcParams对$$=1(本表达式中参数数量为1)
exprlist -> null | exprlist1:null的情况下$$=0(本表达式中参数数量为0,lua2.1的文法分析中总是有意地特殊null,值得学习!)
exprlist1 -> expr | exprlist1, expr:
这个产生式的分析过程相当复杂,变得复杂的主要原因是这种情况“func(1, 2, func2(1,2))”:在分析的过程中,func2的结果当然
要作为参数的一员,那么就要要求这个func2有一个返回值,所以会在指令PUSHGLOBAL index(func2)之后添加一个字节1,表示返回
值数量为1。
表达式func(1,2,func2(1, 2))产生的伪指令如下:
PUSHGLOBAL index(func) PUSH1 PUSH2 PUSHGLOBAL index(func2) PUSH1 PUSH2 CALLFUNC 2 CALLFUNC 3 0
需要注意的是,functioncall可能会在语句或表达式中使用,如果是在语句中,那么其对应的返回值数量一定是0,而在表达式中大部分情况下是1,但是如“a, b = func1()”这种情况下就是2,而“a, b, c = func1(), 2”的时候是1,“a, b, c = 2, func1()”是2。
expr -> expr1 and PrepJump expr1:
语句“c = 0 and 1;”产生的指令为PUSH0 ONFJMP 2 POP PUSH1 STOREGLOBAL index(c)
需要说明的地方是这串指令不会执行PUSH1,因为ONFJMP 2指令会直接跳过PUSH1。
expr -> expr1 or PrepJump expr1:
语句“c = 1 or 0;”产生的指令为PUSH1 ONTJMP 2 PHP PUSH0 STOREGLOBAL index(c)
需要说明的地方是这串指令不会执行PUSH0,因为ONTJMP 2指令会直接跳过PUSH0。