为PL0语言设计一个词法分析器,能够识别PL0语言编写的程序中的单词的合法性。
PL0语言是与pascal类似的语言,但是比pascal语言的词法、语法更加严格苛刻。
在PL0语言中,只有五种类型的单词:关键字、用户自定义标识符、数字、特殊符号以及非法标识符。
根据题目给出的PL0的文法,关键字只有如下几个:
Const——用来声明常量;
Var——用来声明变量;
Procedure——用来声明过程;
Begin——声明过程的开始;
End——声明过程的结束;
Ood——单目运算符,声明为关键字;
If——条件语句;
Then——条件语句;
Call——声明调用函数;
While——循环语句;
Read——读文件语句;
Write——写文件语句;
对于用户自定义标识符应该满足:以字母开头,其后是字母和数字的组合,即:字母(字母|数字)*
数字则是数字字符(0——9)组成的数字串;
特殊符号有如下几个:
+,-,*,/,=<>,<,>=,<=,:=
把关键字、算符和界符称为语言固有的单词,标识符、常量称为用户自定义的单词。
为此设置三个全程量:SYM,ID,NUM。
SYM:存放每个单词的类别,为内部编码的表示形式;
ID:存放用户所定义的标识符的值,即标识符字符串的机内表示;
NUM:存放用户定义的数。
三、设计方案:
(1)对于关键字,应该为每个关键字分配一个类别,该类别在机器内部为内部编码的表示形式;
采用枚举型,将所有的关键字放在一个表中,这样每一个关键字都分配到了一个内部编码;
(2)对于用户自定义的标识符,应该给他们一个统一的类别——SYM_IDENTIFIER;
(3)对于所有的特殊符号(例如+、-等),与关键字的处理方法一样给每一个符号分配一个类别。
(4)上述所有的类别均放在一个枚举类型表中。
(5)对于关键字,还应该有一个字符串类型的表,用来存储每一个关键字的名称。当词法分析器读入一个单词的时候,首先判断是不是关键字时,立马转入到表中,和表中的关键字进行匹配,如果匹配成功,则读入的标识符时关键字,否则进行进一步的判断;
1、滤掉单词间的空格;
2、识别关键字,用查关键字表的方法识别。当单词是关键字时,将对应的类别放在SYM中;
3、识别标识符,标识符的类别为SYM_IDENTIFIER,SYM_IDENTIFIER放在SYM中,标识符本身的值放在ID中。关键字或者标识符的最大长度是10;
4、拼数,将数的类别NUMBER放在SYM中,数本身的值放在NUM中;
5、拼由两个字符组成的运算符,如:>=,<=等等,识别后将类别放在SYM中;
6、错误处理,如果读入不能识别的单词、或者非法的字符、或者长度超过10的字符,就会报错。定义一个全局变量line记录当前读到程序的行数,报错时指示错误的行数;
7、打印源程序,边都入字符表打印;
8、为GETSYM设计一个GETCH函数,该函数读入一个字符;
9、定义一个判断是不是关键字的函数。
C语言开发,首先定义mycode.h文件,定义数据结构和用来查询和显示的数组:
const int KEY_NUM = 13;//关键字个数
const int OP_NUM = 13;//操作符个数
enum SymType
{
//keywords
SYM_BEGIN,//begin
SYM_CALL,//call
SYM_CONST,//const
SYM_DO,//do
SYM_END,//end
SYM_IF,//if
SYM_OOD,//ood
SYM_PROCEDURE,//procedure
SYM_THEN,//then
SYM_VAR,//var
SYM_WHILE,//while
SYM_READ,//read
SYM_WRITE,//write
SYM_IDENTIFIER,//标识符
SYM_NUMBER,//数字
//特殊符号
SYM_PLUS,//+
SYM_MINUS,//-
SYM_TIMES,//*
SYM_SLASH,///
SYM_EQU,//=
SYM_NEQ,//#
SYM_LES,//<
SYM_LEQ,//<=
SYM_GTR,//>
SYM_GEQ,//>=
SYM_LPAREN,//(
SYM_RPAREN,//)
SYM_COMMA,//,
SYM_SEMICOLON,//;
SYM_PERIOD,//.
SYM_BECOMES,//:=
};
int key_table[KEY_NUM] = { SYM_BEGIN, SYM_CALL, SYM_CONST, SYM_DO,SYM_END,
SYM_IF, SYM_OOD, SYM_PROCEDURE, SYM_THEN,SYM_VAR, SYM_WHILE,
SYM_READ,SYM_WRITE };
char *SYM_NAME[] = { "SYM_BEGIN","SYM_CALL", "SYM_CONST", "SYM_DO","SYM_END",
"SYM_IF", "SYM_OOD","SYM_PROCEDURE", "SYM_THEN", "SYM_VAR","SYM_WHILE",
"SYM_READ","SYM_WRITE", "SYM_IDENTIFIER","SYM_NUMBER","SYM_PLUS","SYM_MINUS",
"SYM_TIMES","SYM_SLASH","SYM_EQU","SYM_NEQ","SYM_LES","SYM_LEQ","SYM_GTR",
"SYM_GEQ","SYM_LPAREN","SYM_RPAREN","SYM_COMMA","SYM_SEMICOLON","SYM_PERIOD",
"SYM_BECOMES",
};
char* key[KEY_NUM ] =
{
"begin","call", "const", "do","end","if",
"ood","procedure", "then", "var","while","read","write"
};
其中,数组char *SYM_NAME[]单纯只是为了显示才定义的量,对于机器内部的语法分析和词法分析没有丝毫作用。
函数GETCH():
int getch() {
if((ch = fgetc(infile)) == EOF) {
printf("文件内容已经读完!");
return0;
}
else{
return1;
}
}
每次读入一个字符,一直读到文件的结尾。
函数GetSym()
int getSYM() {//每次得到一个单词,然后对这一个单词进行分析
inti;
//每次读入一个单词到token之前,必须将token清空
chartoken[MAXLENGTH] = { '\0' };
for(i = 0;i
id[i]= '\0';
}
NUM= -1;
getch();
while(ch == ' ' || ch == '\n' || ch == '\t') {//滤掉一个单词之前的空格,记录行号
if(ch=='\n')
line++;
getch();
}
if(ch == EOF) return 0;//如果文件内容已经读完,则返回0
if(isalpha(ch)) {//如果是字母,有可能是标识符或关键字 先判断是不是关键字
intstart = 0;
do{
token[start++]= ch;
getch();
}while (isalpha(ch) || isdigit(ch));
//printf("%s\n",ch);
if(start>10) Error(2); //标识符或关键字最大长度为10
strcpy(id,token);
//printf("%s\n",id);
fseek(infile,-1, 1);//将当前光标位置左移一位
inttt = IsKey(id);//判断是不是关键字
if(tt != -1) {
SYM= tt;
}
else{
SYM= SYM_IDENTIFIER;
}
return1;
}
elseif (isdigit(ch)) {
intn = 0;
do{
n= n * 10 + (ch - '0');
//printf("%d",n);
getch();
}while (isdigit(ch));
SYM= SYM_NUMBER;
NUM= n;
fseek(infile,-1, 1);//将当前光标位置左移一位
return2;
}
elseif (ch == ':') {
getch();
if(ch == '=') {
SYM= SYM_BECOMES;
id[0]= ':';
id[1]= '=';
}
else{
Error(1);
}
}
elseif (ch == '=') {
SYM= SYM_EQU;
id[0]= '=';
}
elseif (ch == '<') {
getch();
if(ch != '=') {
fseek(infile,-1, 1);//将当前光标位置左移一位
SYM= SYM_LES;
id[0]= '<';
}
else{
SYM= SYM_LEQ;
id[0]= '<';
id[1]= '=';
}
}
elseif (ch == '>') {
getch();
if(ch != '=') {
fseek(infile,-1, 1);//将当前光标位置左移一位
SYM= SYM_GTR;
id[0]= '>';
}
else{
SYM= SYM_GEQ;
id[0]= '>';
id[1]= '=';
}
}
elseif (ch == '+') {
SYM= SYM_PLUS;
id[0]= '+';
}
elseif (ch == '-') {
SYM= SYM_MINUS;
id[0]= '-';
}
elseif (ch == '*') {
SYM= SYM_TIMES;
id[0]= '*';
}
elseif (ch == '/') {
SYM= SYM_SLASH;
id[0]= '/';
}
elseif (ch == ';') {
SYM= SYM_SEMICOLON;
id[0]= ';';
}
elseif (ch == ',') {
SYM= SYM_COMMA;
id[0]= ',';
}
elseif (ch == '.') {
SYM= SYM_PERIOD;
id[0]= '.';
}
elseif (ch == '(') {
SYM= SYM_LPAREN;
id[0]= '(';
}
elseif (ch == ')')
{
SYM= SYM_RPAREN;
id[0]= ')';
}
elseif (ch == '#')
{
SYM= SYM_NEQ;
id[0]= '#';
}
else{
Error(0);
}
return3;
}
读入一个单词,如果首字符是字母,先判断是不是关键字,调用IsKey()函数,函数内容如下:
int IsKey(char token[]) {
inti;
for(i = 0;i
if(strcmp(token, key[i]) == 0) {
returni;
}
}
return-1;
}
如果不是关键字,则是标识符(长度<=10).
如果首字符是数字,则一直读取,直到遇到非数字字符为止。
在读入的过程中,采用do——while循环,这样可以向前读一个字符,从而预先判断是不是将要终止读入。
注意:在读入<、>的时候,要再往后读一个字符,判断此时应该读入的字符究竟是<=还是<。
错误处理函数接收一个错误编号,然后打印错误的内容和出现错误的位置等信息:
void Error(int ErrorID) {
switch(ErrorID) {
case0:
printf("Error0:not found %c in line %d\n", ch,line);
break;
case1:
printf("Error1:after colon must be = in line %d\n",line);
break;
case2:
printf("Error2:the length of keyword or indentiy is above 10 in line %d,pleasecorrect\n",line);
break;
}
}
主函数则负责处理读入源文件、写入中间文件和打印、调用GetSym的工作。
int main(int argc, char *argv[]) {
charfileName[MAXLENGTH];
infile= fopen("PL0.txt", "r");
outfile= fopen("PL0_lexical_analyzer.txt", "w");
if(infile == NULL) {
printf("该文件不存在。\n");
getchar();
exit(0);
}
intSYM_TYPE;
while(SYM_TYPE = getSYM()) {
switch(SYM_TYPE) {
case1://关键字和标识符
printf("<%s , %s>\n",SYM_NAME[SYM], id);
fprintf(outfile,"%s",SYM_NAME[SYM]);
fprintf(outfile," ");
fprintf(outfile,"%d", SYM);
fprintf(outfile," ");
fprintf(outfile,"%s", id);
fprintf(outfile,"%c", '\n');
break;
case2://数字
printf("<%s , %d>\n",SYM_NAME[SYM], NUM);
fprintf(outfile,"%s", SYM_NAME[SYM]);
fprintf(outfile," ");
fprintf(outfile,"%d", SYM);
fprintf(outfile," ");
fprintf(outfile,"%d", NUM);
fprintf(outfile,"%c", '\n');
break;
case3://运算符,界符
printf("<%s , %s>\n",SYM_NAME[SYM], id);
fprintf(outfile,"%s", SYM_NAME[SYM]);
fprintf(outfile," ");
fprintf(outfile,"%d", SYM);
fprintf(outfile," ");
fprintf(outfile,"%s", id);
fprintf(outfile," ");
fprintf(outfile,"%c", '\n');
break;
}
}
fclose(infile);
fclose(outfile);
return0;
}
词法分析器的主要功能在于过滤掉源程序中的空格、换行、制表符,判断每个单词的合法性。如果一个单词合法,则要返回它的类别和值,从而为语法分析服务;否则,停止分析并报错。
在设计词法分析器的时候,最难的问题在于分析PL/0文法,找出它的关键字和特殊符号,并给出一个合适的数据结构用来进行查找匹配。一般的结构体和数组都不是很好的选择,但是枚举型却能够很好的解决这个问题。
七、源代码: