本文将讲解如何扩展原有pl0编译器的功能,即增加标题所描述的三个功能。
项目github地址:https://github.com/chaolu0/PL0-extend
该项目为编译原理课程作业,如有错误,欢迎指正。
说一下编译器的三个部分。
词法分析:用来把每个单词拆分出来,并明确单词的类型。
语法分析:读入多个单词,看是否符合既定语法规则。
语义分析:即根据语句操作内存和CPU,以完成功能。
pl0是基于假象栈式计算机的,即所有变量都会存储到一个栈当中,同时有一个指令的数组,通过一个指针来标识当前指令地址,指令的操作都基于栈来实现。栈底部一般都是变量,顶部是用来计算的临时变量。首先明白了这个,才容易理解后面的代码。
注释,用来辅助程序员看懂程序的部分,并不会影响真正代码的运行。由于其只涉及到词法分析部分,我们处理它的方法是:当遇到’{’的时候,我们一直获取下一个字母,一直到出现’}’为止,这部分内容我们直接丢弃掉,如此便可使注释不参加进一步的编译。下面来看一下具体实现。Ps:以下所有带有do结尾的,都是宏定义,实际都是函数。为了方便,直接按照函数来处理
//定位getsymdo
if (ch == '{')
{
do{
if (cc == ll&&ch != '}'){
error(31);//没有匹配的右括号
}
getchdo;//读入下一个字符
} while (ch != '}');
getchdo;//读入下一个符号前,必须先读入下一个字符
getsymdo;//获取下一个符号,下一个符号为真实符号,当前'}'符号没有意义
}
首先,我们需要遇到当前字母是左括号的情况,然后通过do-while循环一直读取下一个字母,直到ch==’}’,在这个循环里面的条件判断是为了处理读完整个符号都没能遇到右括号的情况,即发生括号不匹配的错误。最后一个getsymdo是为了不影响之后程序的运行,当前注释没有意义,所以再读入一个符号作为本次读入的结果。
原有pl0语言只支持if-then,即是只支持if语句,为了增加else,我们先来看一下它是如何实现if跳转的。
//定位statement
if (sym == ifsym)
/* 准备按照if语句处理 */
{
getsymdo;
memcpy(nxtlev, fsys, sizeof(bool) * symnum);
nxtlev[thensym] = true;
nxtlev[dosym] = true;
/* 后跟符号为then或do */
conditiondo(nxtlev, ptx, lev);
/* 调用条件处理(逻辑运算)函数 */
if (sym == thensym) {
getsymdo;
} else {
error(16);
/* 缺少then */
}
cx1 = cx;
/* 保存当前指令地址 */
gendo(jpc, 0, 0);
/* 生成条件跳转指令,跳转地址未知,暂时写0 */
statementdo(fsys, ptx, lev);
/* 处理then后的语句 */
code[cx1].a = cx;
/* 经statement处理后,cx为then后语句执行完的位置,它正是前面未定的跳转地址 */
}
代码有点长,但是我们一条一条来看还是容易看懂的。
第一个getsymdo是为了下面的conditiondo服务,conditiondo上面的三行是用来准备检查下一个符号是不是then或都的,暂时忽略。conditiondo这个函数是用来处理if语句后面的内容,其实就是一个逻辑表达式的处理,执行完这条语句的时候,栈顶会存放计算的结果值,后面jpc指令就是用来根据栈顶值决定要不要跳转的。
cx1=cx;这条语句用cx1记录了下个指令的位置,当前指令即是下面gendo(jpc, 0, 0);所生成的指令jpc,为什么在这里生成了条件跳转指令呢?我们先接着往下看。statementdo(fsys, ptx, lev);这条语句是用来处理then后面的语句,其实就相当于高级语言if为真执行的语句。 code[cx1].a = cx;语句记录了then里面语句执行完之后的下一个地址。现在我们来理解这个过程。我们考虑如果if为真,那么整个程序其实相当于顺序执行,而为假的时候,我们就需要跳过then中的语句,而去执行下面的语句。而它的处理正和我们想法一致:当if为真,条件跳转并不会执行(虽然有语句),当if为假,我们就把then后面语句都走完之后,把下一个条指令的地址放到条件跳转的地址当中去,这样就可以实现跳过then中间的语句。
我们考虑一下if-else,增加else之后,if为假实际上没有什么影响,因为正好跳转到了else的位置,但是if为真的时候,我们需要跳过的就是else语句部分了。根据原有代码,我们给出下面的代码:
if (sym == ifsym)
/* 准备按照if语句处理 */
{
getsymdo;
memcpy(nxtlev, fsys, sizeof(bool) * symnum);
nxtlev[thensym] = true;
nxtlev[dosym] = true;
nxtlev[elsesym] = true;
/* 后跟符号为then或do */
conditiondo(nxtlev, ptx, lev);
/* 调用条件处理(逻辑运算)函数 */
if (sym == thensym) {
getsymdo;
} else {
error(16);
/* 缺少then */
}
cx1 = cx;
/* 保存当前指令地址 */
gendo(jpc, 0, 0);
/* 生成条件跳转指令,跳转地址未知,暂时写0 */
statementdo(fsys, ptx, lev);
/* 处理then后的语句 */
cx3 = cx;
gendo(jmp, 0, 0); //将来会直接跳转到else语句后面
code[cx1].a = cx;
/* 经statement处理后,cx为then后语句执行完的位置,它正是前面未定的跳转地址 */
if (sym == elsesym) {
getsymdo;
statementdo(fsys, ptx, lev);
code[cx3].a = cx; //当前是else后面的语句结束位置,if语句执行后应当跳转至此
}
}
实际上我们就是在if的then语句执行完毕后,增加了一个直接跳转jmp,因为如果执行了then,说明一定不会执行else,应该直接跳过else,跳过的方式如上面所述。
至此if-else也完美解决。Ps:有些小细节,比如else关键字什么的自行处理一下就好~
数组是这里面最难的部分,我们需要理解里面的几个函数,首先看下enter(填写名字表函数)
/* 名字表结构 */
struct tablestruct
{
char name[al]; /* 名字 */
enum object kind; /* 类型:const, var, array or procedure */
int val; /* 数值,仅const使用 */
int level; /* 所处层,仅const不使用 */
int adr; /* 地址,仅const不使用 */
int size; /* 需要分配的数据区空间, 仅procedure使用 */
};
void enter(enum object k, int* ptx, int lev, int* pdx)
{
(*ptx)++;
strcpy(table[(*ptx)].name, id); /* 全局变量id中已存有当前名字的名字 */
table[(*ptx)].kind = k;
switch (k)
{
case constant: /* 常量名字 */
if (num > amax)
{
error(31); /* 数越界 */
num = 0;
}
table[(*ptx)].val = num;
break;
case variable: /* 变量名字 */
table[(*ptx)].level = lev;
table[(*ptx)].adr = (*pdx);
(*pdx)++;
break;
case procedur: /* 过程名字 */
table[(*ptx)].level = lev;
break;
}
}
名字表(是一个结构体数组),用来存储所有变量,常量,过程的东西,里面记录的字段如上面结构体所示,ptx是数组的尾指针,用来插入下一个值。我们要增加的数组也属于变量,所以我们看变量部分,首先是层次,不用关心,照写就可以,然后是地址,地址我们需要变化。因为一个普通变量只占1个位置,而我们的数组是多个位置,这里就是分配内存的地方,所以我们要+=,而不是++。上代码
/*
* start: 数组开始位置
* end: 数组结束为止
*
* 其他变量同enter方法
*/
void enterArray(int* ptx, int lev, int*pdx, int start, int end,char* id){
(*ptx)++;//增加名字表内容
strcpy(table[(*ptx)].name, id);
table[(*ptx)].kind = array;//数组类型
table[(*ptx)].level = lev;//层次
table[(*ptx)].adr = (*pdx);//数组首地址
table[(*ptx)].low = start;//用来记录初始的下标值,例如:定义a(10:11),low就是10,将来我们访问元素的时候应该减掉这个值,因为我们实际分配给数组的地址偏移量是从0开始的。即a(10)实际上是a(0);
(*pdx) += (end - start + 1);//连续内存地址分配给该数组
}
这段代码比较简单,实际上就是看定义符不符合数组定义,例如a(b,12);我们需要从中获取b和12的值,然后调用上面的enterArray方法。由于逻辑比较简单,不再赘述。
读、写、赋值、使用都需要单独处理,我们现在以读入为例讲解,其余实现都比较类似。
我们先来看原有的读入是怎么写的
if (table[i].kind == variable) { //read(a);
gendo(opr, 0, 16);
/* 生成输入指令,读取值到栈顶 */
gendo(sto, lev - table[i].level, table[i].adr);
/* 储存到变量 */
getsymdo;
}
第一句生成输入指令,将来就会把读入的值放到栈顶,第二句,根据名字表中存储的该变量的地址,把栈顶的值放到那个地址中,然后获取下个符号,进行后序的操作。
现在考虑我们读入数组,假如我们有定义a(0:3),将来要read(a(1))。我们在名字表中能查找到的只有a,也就是a(0)的地址,那我们怎么知道a(1)在哪里存储呢?其实就是基地址+偏移量。基地址就是数组首地址,即a的地址,偏移量就是1,a(1)的地址就是a.addres+1。考虑到可能有read(a(0+1))这样的情况,我们使用原有的表达式处理函数。
expressiondo这个函数会把表达式的值放到栈顶,这并不符合我们的要求,我们希望他给我们返回一个值,然后使用sto指令,把地址传进去。那我们怎么解决这个问题?答案就是我们也把处理延后。
既然偏移量在栈顶,我们把基地址也放到栈顶,然后相加,那么栈顶的就是真实地址,然后我们就会发现sto指令并不能满足我们,因为它使用的是名字表的地址,而我们的地址在栈顶,所以我们自己再写一个指令sto2来完成我们需要的功能
case sto:
/* 栈顶的值存到相对当前过程的数据基地址为a的内存 */
t--;
s[base(i.l, s, b) + i.a] = s[t];
break;
case sto2:
t--;
s[base(i.l, s, b) + s[t - 1]] = s[t];
break;
很简单,基本没有变化,只不过sto的地址是传入的,而sto2的地址是从栈顶拿到的,这样我们就解决了找到数组元素并赋值的过程~看代码
if (table[i].kind == variable) { //read(a);
gendo(opr, 0, 16);
/* 生成输入指令,读取值到栈顶 */
gendo(sto, lev - table[i].level, table[i].adr);
/* 储存到变量 */
getsymdo;
} else {
getsymdo;
expressiondo(nxtlev, ptx, lev, true, i); //括号内的表达式,将偏移量放到栈顶
gendo(lit, 0, table[i].adr); //基地址
gendo(opr, 0, 2); //当前栈顶是真实地址
gendo(opr, 0, 16);
/* 生成输入指令,读取值到栈顶 */
gendo(sto2, lev - table[i].level, 0);
}
代码很简单,看一下就懂~,这样我们就完成了~。剩下的写,赋值,使用都类似,只说一下要修改的地方,不再展开讨论。
lod指令,它与sto存在问题相同,需要修改
expressiondo,由于存在数组的运算,所以需要修改这个函数。
词法部分,语法部分,数组导致这两个部分有变化,但修改起来不难。例如数组定义时使用的冒号,需要增加这个符号。实现了这几个部分,整个扩展就完成了,完成后面这几部分,将会让你的理解更加深刻!
**本人大学生程序员一名,以上只是写的过程中的经验之谈,如有错误,欢迎指正!
热爱代码,所以奋斗!**