编译原理实验(一)—— 词法分析程序的设计与实现

本文最后更新于:2019年11月15日 晚上

概览:基于《编译原理(第三版)》(清华大学出版社,王生原版)的PL/0词法分析程序。

实验内容

通过对PL/0词法分析程序(GETSYM)的分析,并在此基础上按照课本1.4节中给出的PL/0语言的语法描述,编写一个PL/0语言的词法分析程序。然后使用该词法分析程序实现对课本12页的图1.19中的源程序的词法分析。

要求:输入为字符串(即待进行词法分析的源程序),输出为二元式,表示为:(单词种别、单词自身的值)。详细见P38。有一定检查词法错误的能力,例如发现2a这类不能作为单词的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var m,n,r,q;
r := 2a;
procedure gcd;
begin
while r#0 do
begin
q := m /n;
r := m - q * n;
m := n;
n := r;
end
end;
begin
read(m);
read(n);
if m<n then
begin
r := m;
m := n;
n := r;
end;
begin
r := 12s;
r := 12ss;
call gcd;
write(m);
end;
end.

算法描述

这个词法分析程序需要以一个文件作为读入文件,读取文件内的内容,识别其中的各种PL0语言的关键字、标识符等。本程序是一次读取一行字符串,这样方便获取文件的行号。

读取一行字符串,字符串的结束标志为’\0’.

读取一行之后如果遇到空格或者缩进之类的全部略过向后读取。如果遇到字母就开始暂存到一个char数组,一直读取直到遇到空格、回车、缩进以及各种符号之类的停止读取。这就是获取到的一个token,使用这个token对PL0语言的保留字表进行检查,如果有相同的,那么这个token就是关键字,如果没有相同的那就是一个标识符。如果读到的内容以数字开头就暂存入char数组,继续向后读取,如果碰到空格或者换行缩进就停止读取,则获取到一个数字,如果碰到字母之类的说明出错,这是错误的标识符,向后读完整个错误的标识符,然后打印错误。

之后就是符号:了,如果向后读取不是=,那么报错,是无法单独出现的。

对于大于号>、小于号<都要去考虑后面有无等于号=,如果有那就是两个符号共同组合,如果没有就是单独的符号。剩下的就是对PL0语言的单个运算符以及单个界符的判断了。然后如果遇到未定义的符号也要报出相应的错误。

参考PL/0词法规则状态转换图

程序结构

void init()函数 :负责将关键字、操作符和界符初始化赋值给全局变量。

void getsym(string str,int index,int lines)函数:负责对字符串str从它的下标index开始进行词法分析,lines表示当前分析的第几行。

void errors(int lines,int err_num,string errstr)函数:显示错误的函数,显示错误出现在第几行lines上,err_num表示错误类型,errstr表示错误造成的原因。

main()函数,这个程序的入口,进行流程控制以及结果显示。

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#include <iostream>
#include <fstream>
#include <string>
#include <list>
#include <algorithm>
#include <conio.h>

using namespace std;

list<string> keyword_list;
list<char> operator_list;
list<char> board_list;

void init();
void getsym(string str,int index,int lines);
int printchar(char *str, int len);
void errors(int lines, int err_num,string errstr);

int main()
{
init();
ifstream readfile;
readfile.open("resourse.txt", ios::in);
string tmp;
int lines = 1;
while (getline(readfile,tmp))
{
cout <<"Lines"<<lines<<": {"<< tmp <<"}"<< endl;
getsym(tmp, 0, lines);
lines++;
cout << endl;
}
_getch();
return 0;
}

void init()
{
string keyword[13] = { "begin", "call" ,"const" ,"do" ,"end" ,"if" ,
"odd" ,"procedure" ,"read" ,"then" ,"var" ,"while" ,"write" };
keyword_list.assign(keyword,keyword+ sizeof(keyword) / sizeof(keyword[0]));

char operators[8] = {'+','-','*','/','=','#','<','>'};
operator_list.assign(operators, operators + sizeof(operators) / sizeof(operators[0]));

char boards[5] = { '(',')',',',';','.' };
board_list.assign(boards, boards + sizeof(boards) / sizeof(boards[0]));

}

void getsym(string str,int index,int lines)
{
char ch = str[index];
/*如果遇到字符串结束符,结束*/
if (ch == '\0') return;
while (ch == ' ' || ch == '\t')/*忽略空格和TAB*/
{
index++;
ch = str[index];
}
if (ch == '\0') return;

/*以字母开头*/
if (ch >= 'a' && ch <= 'z')/*如果程序是以字母a~z之间开头的,就继续读取,知道遇到空格、换行或者TAB结束*/
{
int word_index = 0;
char new_word[100];
new_word[word_index] = ch;

while(str[index + 1] != '\0' && (str[index + 1] >= 'a' && str[index + 1] <= 'z') || (str[index + 1] >= '0' && str[index + 1] <= '9'))
{
word_index++;
index++;
new_word[word_index] = str[index];
}
/*当读取完一个完整的单词之后,搜索是不是保留字*/
string strs = string(new_word, new_word + word_index+1);

/*如果是保留字,输出*/
if (find(keyword_list.begin(), keyword_list.end(), strs) != keyword_list.end())
{
cout<< "保留字 —— " << strs << endl;
}

/*如果不是保留字,则就是标识符号*/
else cout << "标识符 —— " << strs << endl;
}

/*数字开头*/
else if (ch >= '0' && ch <= '9')/*如果单词是以数字开头的,就继续读取,遇到空格、换行或者TAB*/
{
int word_index = 0;
char new_word[100];
new_word[word_index] = ch;

while (str[index + 1] != '\0' && str[index + 1] >= '0' && str[index + 1] <= '9')
{
word_index++;
index++;
new_word[word_index] = str[index];
}

/*如果数字中碰到了字母,那么就是词法错误*/
if (str[index + 1] != '\0' && str[index + 1] >= 'a' && str[index + 1] <= 'z')
{
string errstr = string(new_word,new_word+word_index+1);
errstr.append(1,str[index+1]);
index++;
while (str[index + 1] != '\0' && str[index + 1] >= 'a' && str[index + 1] <= 'z')
{
errstr.append(1, str[index + 1]);
index++;
}
errors(lines, 1,errstr);
}
else
{
cout << "数字 —— ";
printchar(new_word, word_index);
cout << endl;
}
}

/*:=*/
else if (ch == ':')
{
if (str[index + 1] == '=')
{
index++;
cout << "运算符 —— :=" << endl;
}
/*出错*/
else if (str[index + 1] != '=')
{
string errstr = "";
errors(lines, 0,errstr);
}
}

/*<=*/
else if (ch == '<')
{
if (str[index + 1] == '=')
{
index++;
cout << "运算符 —— <=" << endl;
}
else cout << "运算符 —— <" << endl;
}

/*>=*/
else if (ch == '>')
{
if (str[index + 1] == '=')
{
index++;
cout << "运算符 —— >=" << endl;
}
else cout << "运算符 —— >" << endl;
}

/*其他单符号*/
else
{
/*如果其他单符号属于已经定义的符号,那么正确,输出二元组*/
if (find(operator_list.begin(), operator_list.end(), ch) != operator_list.end())
{
cout << "运算符 —— " << ch << endl;
}
else if (find(board_list.begin(), board_list.end(), ch) != board_list.end())
{
cout << "界符 —— " << ch << endl;
}
else
{
string errstr = string(1, ch);
errors(lines, 2, errstr);
}
}

getsym(str, index+1, lines);
return;
}

int printchar(char *str, int len)
{
for (int i = 0; i <= len; i++)
{
cout << str[i];
}
return 0;
}

void errors(int lines, int err_num, string errstr)
{
cout << "出错在" << lines << "行" << "原因是: " ;
switch (err_num)
{
case 0:
cout << "{ : 无法单独使用,需要联合 = 组成:= 才能一起使用 }" << endl;
break;
case 1:
cout << "{ : "<< errstr <<" 是错误的数字或者错误的标识符 }" << endl;
break;
case 2:
cout << "{ :" << errstr << " 符号未定义 }" << endl;
}
}

结果展示

设计技巧

标识符、运算符和界符存放进了C++ STL的list链表中,这样查询时比较方便。然后读取文件是按行进行读取,getsym()函数一次处理一行的数据,当此函数识别完一个单词或者数字后,如果这一行并没有结束,他会递归调用自己直到处理完这一行为止。此外报错函数errors()里使用了switch case语句,可以给不同的错误类型赋予不同的编号,使用时传入相关参数直接调用。

心得体会

通过本次实验我了解到了一个词法分析器的实现过程,根据字符的识别来分析得到token,同时也明白了一个词法分析器实现的复杂,需要考虑各种各样的情况,同时也应该考虑设计之后针对语法分析器所留的接口之类的问题,总之通过这次实验收获了很多,更加对编译原理有了浓厚的兴趣。