C语言解释器的实现--语法解析(五)
1.代码块
代码块是由多个表达式组成的一组代码。它可以看成是以下的形式:
{
exp1
exp2
...
}
它由"{"开始,由"}"结束,中间包含多条表达式,或者是控制语句。如果不是以"{"开始,那么,一个代码块就是一条表达式。在上面的章节,我们已经介绍过了,每个表达式会产生一个中间代码。它是一个链表 struct _code * ,而一个代码块,是由多个表达式组成的,所以我们将每个表达式的中间代码链表连到一起就成了代码块的中间代码了。
如果代码块中包含控制语句,那么,我们必须做一些处理,即在代码链表中插入跳转语句,和跳转位置(Lab)。
2.控制语句
2.1 C语言中,控制语句有这些:
a. if( exp ) stmt else stmt
b. do stmt while( exp )
c. while( exp ) stmt
d. for( exp1; exp2; exp3 ) stmt
e. switch( exp ) stmt
f. goto lab
其中,stmt表示一个代码块。我们如何为这些代码产生中间代码呢?这里还要说明的是跳转语句。比如一个if语句:
if( exp ) stmt1 else stmt2
那么,它的意思是,当 exp == 0 时,跳转到stmt2位置;当exp != 0的时候不做跳转,但是stmt1执行完成后要跳转到stmt2的后面。所以,这中间涉及了两个东西:跳转语句 和 跳转的位置。跳转语句我们用三种命令表示:JE、JNE、JMP,即不等于跳转,等于跳转,无条件跳转。 跳转的位置我们用Lab表示,即在代码链表中插入一个标签,供跳转语句查找要跳转的位置。
还是上面的if语句,它产生后的代码应该是这样的:
A. if( exp ) stmt1 else stmt2 -->
exp
JE L1
stmt1
JMP L2
L1:
stmt2
L2:
其中,L1 L2分别占用代码链表的一个节点,在code_t结构体中,用lab域表示。
2.2 控制语句中的break和continue.
在一些控制语句中,他们支持break和continue,即如果在代码块总出现break,那么他应该跳转到代码块的外面,如果是continue,那么跳转到条件语句继续执行。例如下面的do while语句:
B. do stmt while( exp ) -->
L1:
stmt <-- 如果这里出现break,那么JMP L3; 如果出现continue, 那么JMP L2
L2:
exp
JNE L1
L3:
因为在解析stmt的时候,L1,L2和L3都已经固定好了,所以,在处理break和continue的时候,跳转的LAB都已经明确,可以用参数将L2和L3传递个stmt()函数,stmt函数中解析break和continue的时候,仅仅是添加一条跳转语句。
2.3 其他控制语句的代码形式:
C. while( exp ) stmt -->
JMP L2
L1:
stmt
L2:
exp
JNE L1
L3:
D. for( exp1; exp2; exp3 ) stmt -->
exp1
JMP L3
L1:
stmt
L2:
exp3
L3:
exp2
JNE goto L1
L4:
E. switch( exp ){
case 1: stmt1
case i: stmti
default: stmt
...
}
exp
selete i and jmp(L1..Ln,L)
Li: stmti
L: stmt
LL:
selete i and jmp(L1..Ln,L) 表示 如果exp的结果是i,那么跳转到Li,否则跳转到L。switch语句跟别的控制语句不一样,其他的控制语句在还没解析代码块的时候,我们就已经知道应该创建几个Lab了,所以我们可以事先创建好Lab,然后在适当的位置插入JMP语句,这个JMP语句中跳转到的Lab这时候已经确定了。但是对于switch语句,我们事先不知道case在什么地方,所以不知道"selete i and jmp(L1..Ln,L)"应该对应什么代码。所以,我们必须解析完stmt(代码块)之后才能产生代码。 具体的做法是在解析代码快的时候记录下所以的Lab,解析完成后再做相应的处理,即构造"selete i and jmp(L1..Ln,L)"代码,将它连接到中间代码的前面。
F. goto Lab -->
JMP Lab
在解析goto的时候,必须将"Lab"名称转换成我们的Lab的表示形式。
3.局部变量的生命周期
在一个函数中定义的变量称之为局部变量,但是局部变量有自己的生命周期,即在自己的代码块中定义的,那么它只对这个代码块的代码可见。例如有下面的代码:
{
int a;
{
int a;
}
printf("%d\n", a);
}
那么第二个a对printf语句处是不可见的。为了表示变量的生命周期,我们为每个变量加入了begin和end域,用来保存该变量对[begin,end]区间的代码是可见的。所以,这里begin,和end怎么解析是个问题,begin不难,在解析定义的时候就可以确定,但是end确实比较难,因为必须在一个代码块中结束后(即解析到"}"后),才知道end的值。所以为了确定end的值,栈在这里又被征用了。
{ <-- 代码块开始,创建一个stack1
int a; <-- 解析完a,将a压入stack1, 此时 a.begin已经确定
{ <-- 遇到"{" 递归调用解析函数,创建一个stack2
int a; <-- 解析完a,将a压入stack2, 此时 a.begin已经确定
} <-- 遇到"}",表示该代码块完成,将a从stack2中pop出来,设置a.end !
此时,递归调用结束。返回到上一个代码块处理函数。
printf("%d\n", a); <--
} <-- 到"}",表示该代码块完成,将a从stack1中pop出来,设置a.end !
经过上面的过程,第一个a和第二个a的begin和end值都被确定。在代码的处理过程中,我们根据变量名查找变量时,必须根据当前代码的位置,来判断位置是否属于[begin,end]区间,而不仅仅是判断变量名。
4.函数解析
一个函数包括这几个部分:
a. 返回值类型
b. 形参列表
c. 局部变量
d. 代码块
例如下面的函数:
int add( int a, int b )
{
int c;
c = a + b;
return c;
}
那么它的返回值类型是int, 参数列表是a、b,局部变量有c, 执行代码是 " c = a + b; return c; " 。仔细观察,它其实是由函数声明和一个代码块组成的。所以解析这个函数也很简单,其实就是解析声明,得到函数名,参数列表和返回值类型。然后执行上一章节描述的解析代码块函数,得到该函数的中间代码链。
5.附
比如有如下的代码:
int main( int argc, char **argv ){
int a, b;
b = 1;
for( a=0; a<10; a++ ){
b *= 2;
}
return b;
}
那么这个函数所对应的中间代码是这样的:
fun: main 2-args: argc argv b a
@0 = b = 1
@1 = a = 0
JMP 7
LAB_5:
@4 = b *= 2
LAB_6:
@3 = a ++
LAB_7:
@2 = a < 10
JNE 5
LAB_8:
@5 = b