实验要求
【任务介绍】递归下降的语法分析。
【输入】一个完整的源程序。
【输出】语法树或者错误。
【题目】设计一个程序,输入字符串形式的源程序,输出该程序的语法分析树,有错误时报告错误。要求:
1.源语言及其语法规则:可以参照附录A,也可以自定义。
2.输入为字符串形式的源程序,因此,需要调用前面实验做过的词法分析器,为语法分析器提供单词符号。
3.应该指出错误的具体位置,如:在xx单词之后/之前发现错误,分析中止。
编译环境和语言
编程语言:C++
IDE:vs 2019
实验原理分析
根据附录A提供的文法,结合我自己构造的一些测试数据,在消除左递归之后,得到的文法如下所示:
<Prog> → <Head> <Main>
<Head> → # include < <String> > <Head> | empty
<Main> → <Type> main ( <Type1> ) <Block>
<Type1> → void | empty
<Block> → { <Decls> <STMTS> }
<Decls> → <Type> <NameList> ; <Decls> | empty
<NameList> → <Name> <NameList1>
<NameList1> → , <Name> <NameList1> | empty
<Type> → int
<Name> → id
<STMTS> → <STMT> <STMTS> | empty
<STMT> → <Name> = <Expr> ;
<STMT> → if ( <BOOL> ) <STMT> <STMT1>
<STMT1> → else <STMT> | empty
<STMT> → while ( <BOOL> ) <STMT>
<STMT> → <Block>
<STMT> → return number ;
<BOOL> → <Expr> <RelOp> <Expr>
<RelOp> → < | <= | > | >= | == | !=
<Expr> → <Term> <Expr1>
<Expr1> → <AddOp> <Term> <Expr1> | empty
<Term> → <Factor> <Term1>
<Term1> → <MulOp> <Factor> <Term1> | empty
<Factor> → id | number | ( <Expr> )
<AddOp> → + | -
<MulOp> → * | / | %
对于上面的文法定义,
因为附录A提供的文法是<Prog> → <Block>
,我认为这有一些不适用的地方,比如无法表示宏定义导入库函数,也无法表示函数返回值类型以及函数名,因此我对其进行了一些延伸,我的文法定义<Prog> → <Head> <Main>
能够接受宏定义,也能够接受函数返回值类型和函数名,但是只能够接受main()函数,以及int的返回值类型。
并且我对的文法定义中,可以发现可以没有宏定义而直接进入到main()函数部分<Head> → # include < <String> > <Head> | empty
。
而且我的表达式语句只能够接受赋值语句、if语句(可以用花括号括起来)、if-else语句(可以用花括号括起来)、while语句(可以用花括号括起来)、代码块、return语句,同时return语句只能够接受数字,不能够使用如printf()函数等库函数。
其他和前面实验六的没有太大的区别,可以发现,还是具有比较多的局限性,但是一些关键部分的构造基本都已经覆盖了。
程序关键部分分析
定义
char s[100][100] = { "\0" }; //用来存储初始数据
string str; //用来存储整合后的数据
int location = 0; //用来定位算术表达式
bool flag = true; //用来判断该算术表达式是否合法
string tree_map[100]; //用来存储语法树
const int width = 3; //设置间隔为3
char token[100] = { "\0" }; //用来暂存单词
string error; //用来记录错误信息
bool isKey(char* s);
bool isOP(char* s);
bool isDE(char& s);
void pre_process(char* buff, int& in_comment);
bool scanner(int k);
int draw_line(int row, int num);
void string_out(string s, int row, int column, int loc);
int tree_out(string s, int row, int loc);
void printTree(ofstream& fout);
int readToken();
void bindString(int k);
int Prog(int row, int column);
int Head(int row, int column);
int Main(int row, int column);
bool Type1(char* words);
int Block(int row, int column);
int Decls(int row, int column);
int NameList(int row, int column);
int NameList1(int row, int column);
bool Type(char* words);
bool Name(char* words);
int STMTS(int row, int column);
int STMT(int row, int column);
int STMT1(int row, int column);
int BOOL(int row, int column);
bool RelOp(char* words);
int Expr(int row, int column);
int Expr1(int row, int column);
int Term(int row, int column);
int Term1(int row, int column);
int Factor(int row, int column);
bool AddOp(char* words);
bool MulOp(char* words);
关键部分分析
词法分析,直接复制实验六中的两个函数:
void pre_process(char* buff, int& in_comment) { //预处理
char data[100] = { '\0' }; //用来存储处理过的数据
char old_c = '\0'; //用来存储上一个字符
char cur_c; //用来存储当前字符
int i = 0; //计数器,记录buff
int j = 0; //计数器,记录data
while (i < strlen(buff)) { //去注释
cur_c = buff[i++]; //首先将获取的字符存入缓存中
switch (in_comment) {
case 0:
if (cur_c == '\"') { //进入双引号中
data[j++] = cur_c;
in_comment = 3;
} else if (cur_c == '\'') { //进入单引号中
data[j++] = cur_c;
in_comment = 4;
} else if (old_c == '/' && cur_c == '*') { //进入多行注释中
j--;
in_comment = 1;
} else if (old_c == '/' && cur_c == '/') { //进入单行注释中
j--;
in_comment = 2;
} else { //其他情况则直接将数据写入data中
data[j++] = cur_c;
}
break;
case 1:if (old_c == '*' && cur_c == '/') in_comment = 0; //多行注释结束
break;
case 2:if (i == strlen(buff)) in_comment = 0; //单行注释到这行结束时标志位置为0
break;
case 3:
data[j++] = cur_c;
if (cur_c == '\"') in_comment = 0;
break;
case 4:
data[j++] = cur_c;
if (cur_c == '\'') in_comment = 0;
break;
}
old_c = cur_c; //保留上一个字符
}
i = 0;
int k = 0;
while (k < j) { //分隔词
if (isalpha(data[k]) || data[k] == '_') { //若为字母或_
while (!isDE(data[k]) && strchr("+-*/%=^~&|!><?:,", data[k]) == NULL && !isspace(data[k])) {
buff[i++] = data[k++];
}buff[i++] = ' ';
} else if (isdigit(data[k])) { //若为数字
while (isdigit(data[k])) {
buff[i++] = data[k++];
}buff[i++] = ' ';
} else if (isspace(data[k])) {
while (isspace(data[k])) { //若为空白字符
k++;
}
} else if (isDE(data[k])) { //若为界符
buff[i++] = data[k++];
buff[i++] = ' ';
} else if (data[k] == '\"') { //若为双引号
buff[i++] = data[k++];
while (data[k] != '\"') buff[i++] = data[k++];
buff[i++] = data[k++];
buff[i++] = ' ';
} else if (data[k] == '\'') { //若为单引号
buff[i++] = data[k++];
while (data[k] != '\'') buff[i++] = data[k++];
buff[i++] = data[k++];
buff[i++] = ' ';
} else if (strchr("+-*/%=^~&|!><?:,", data[k]) != NULL) { //若为运算符,再查看下一个字符,要尽可能多包含一些运算符
switch (data[k]) {
case '+':buff[i++] = data[k++];
if (data[k] == '+' || data[k] == '=') buff[i++] = data[k++]; //为++或+=运算符
break;
case '-':buff[i++] = data[k++];
if (data[k] == '-' || data[k] == '=' || data[k] == '>') buff[i++] = data[k++]; //为--或-=或->运算符
break;
case '*':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为*=运算符
break;
case '/':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为/=运算符
break;
case '%':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为%=运算符
break;
case '=':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为==运算符
break;
case '^':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为^=运算符
break;
case '&':buff[i++] = data[k++];
if (data[k] == '&' || data[k] == '=') buff[i++] = data[k++]; //为&&或&=运算符
break;
case '|':buff[i++] = data[k++];
if (data[k] == '|' || data[k] == '=') buff[i++] = data[k++]; //为||或|=运算符
break;
case '!':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为!=运算符
break;
case '>':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为>=运算符
else if (data[k] == '>') {
buff[i++] = data[k++]; //为>>运算符
if (data[k] == '=') buff[i++] = data[k++]; //为>>=运算符
}break;
case '<':buff[i++] = data[k++];
if (data[k] == '=') buff[i++] = data[k++]; //为<=运算符
else if (data[k] == '<') {
buff[i++] = data[k++]; //为<<运算符
if (data[k] == '<') buff[i++] = data[k++]; //为<<=运算符
}break;
default:buff[i++] = data[k++];
}buff[i++] = ' ';
} else if (data[k] == '#') {
buff[i++] = data[k++];
buff[i++] = ' ';
}
}
buff[i] = '\0'; //处理完以后,会在最后留上一个空格
}
bool scanner(int k) { //词法分析处理
int in_comment = 0; //0表示没问题,1表示在多行注释中,2表示在单行注释中,3表示在双引号中,4表示在单引号中
for (int i = 0; i < k; i++) {
pre_process(s[i], in_comment); //首先预处理,去掉注释,词与词之间、词与运算符之间用一个空格隔开
}
if (in_comment != 0) return false; //若标志位不等于0,则说明多行注释不到位,没有结束标志
else return true;
}
其次是构造语法树的一些相关函数以及对输入数据进行处理的一些函数,还是直接从实验六复制过来的,稍有不同的是bindString()函数,因为这次是读文件的形式,考虑到正常的程序中不会在最后加一个#,因此需要在函数中所有数据整合到一起后我自己人为地在最后加上一个#:
int draw_line(int row, int num) { //用来画横线,隔开兄弟节点,返回下次开始的起始位置
tree_map[row].append(num, '-');
return tree_map[row].size();
}
/**用来输出字符串
* 其中column为该行的起始位置,loc为上一行竖线的位置,
* loc默认为0,表示没有竖线,则此时通过column将该字符串放入到相应位置
* 若不为0,则通过loc对该字符串进行位置的处理
*/
void string_out(string s, int row, int column, int loc = 0) {
if (loc == 0) {
if (tree_map[row].size() < column) { //若不等,则说明中间需要填充空格
int n = column - tree_map[row].size();
tree_map[row].append(n, ' ');
}
tree_map[row].append(s);
} else {
int n1 = s.size() / 2;
if (loc - n1 <= column) { //若该节点的长度比父节点长,则还是通过column添加
if (tree_map[row].size() < column) { //若不等,则说明中间需要填充空格
int n = column - tree_map[row].size();
tree_map[row].append(n, ' ');
}
tree_map[row].append(s);
} else { //这种情况必须填充空格
int n = loc - n1 - tree_map[row].size();
tree_map[row].append(n, ' ');
tree_map[row].append(s);
}
}
}
/**画父子节点之间的竖线,s表示父亲节点的字符,loc表示父亲节点的起始位置
* 返回值用于处理运算符的位置
*/
int tree_out(string s, int row, int column) {
int n1 = s.size() / 2;
int n2 = column + n1 - tree_map[row].size();
tree_map[row].append(n2, ' ');
tree_map[row] += '|';
return n1 + column;
}
void printTree(ofstream& fout) {
for (int i = 0; i < 100; i++) {
if (!tree_map[i].empty()) {
//cout << tree_map[i] << endl;
fout << tree_map[i] << endl;
} else break;
}
}
int readToken() { //用来根据空格从str中取词,并返回该词的长度,以便进行移位操作
int i = 0;
for (; str[location + i] != ' '; i++) {
token[i] = str[location + i];
}
token[i] = '\0';
return i;
}
void bindString(int k) { //用来将s数组中的内容整合到str中
for (int i = 0; i <= k; i++) {
str.append(s[i]);
}
str += '#'; //在最后加上一个#作为结束标志
}
然后最关键的部分就是文法的实现,这些文法的实现都是严格根据前面的文法定义构造的,其中因为需要报错并给出错误的位置,所以前面声明了一个string类型的error来存储错误信息,并且报错提示都是指出在当前读到的词token之前缺少期待的词,这里还需要注意的一点是,我在每次递归调用文法函数时,回溯之后都会先判断flag是否为false,if (!flag) return 0;
,若为false,则直接返回,这是为了保证错误信息不会被错误地更改:
int Prog(int row, int column) {
if (flag) {
string_out("<Prog>", row, column);
int loc = tree_out("<Prog>", ++row, column);
int num1 = Head(++row, column);
if (!flag) return 0;
column = draw_line(row, num1 + width);
int num2 = Main(row, column);
if (!flag) return 0;
return num1 + num2 + width + 6;
}
}
int Head(int row, int column) {
if (flag) {
string_out("<Head>", row, column);
int loc = tree_out("<Head>", ++row, column);
int i = readToken();
if (strcmp(token, "#") == 0) {
location = location + i + 1;
string_out(token, ++row, column, loc);
column = draw_line(row, width);
i = readToken();
if (strcmp(token, "include") == 0) {
location = location + i + 1;
string_out(token, row, column);
column = draw_line(row, width);
i = readToken();
if (strcmp(token, "<") == 0) {
location = location + i + 1;
string_out(token, row, column);
column = draw_line(row, width);
i = readToken(); //必是<String>
location = location + i + 1;
string_out("<String>", row, column);
loc = tree_out("<String>", row + 1, column);
string_out(token, row + 2, column, loc);
column = draw_line(row, width);
i = readToken();
if (strcmp(token, ">") == 0) {
location = location + i + 1;
string_out(token, row, column);
column = draw_line(row, width);
int num1 = Head(row, column);
if (!flag) return 0;
return num1 + width * 5 + 7 + 1 + 8 + 1 + 6;
} else {
string s = token;
error = s + "之前缺少>";
flag = false;
return 0;
}
} else {
string s = token;
error = s + "之前缺少<";
flag = false;
return 0;
}
} else {
string s = token;