编译原理实验七:对完整程序的递归下降语法分析并构造语法树

实验要求

【任务介绍】递归下降的语法分析。

【输入】一个完整的源程序。

【输出】语法树或者错误。

【题目】设计一个程序,输入字符串形式的源程序,输出该程序的语法分析树,有错误时报告错误。要求:

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;
               
递归下降分析法 一、实验目的: 根据某一文法编制调试递归下降分析程序,以便对任意输入的符号串进行分析。本次实验的目的主要是加深对递归下降分析法的理解。 二、实验说明 1、递归下降分析法的功能 词法分析器的功能是利用函数之间的递归调用模拟语法树自上而下的构造过程。 2、递归下降分析法的前提 改造文法:消除二义性、消除左递归、提取左因子,判断是否为LL(1)文法, 3、递归下降分析法实验设计思想及算法 为G的每个非终结符号U构造一个递归过程,不妨命名为U。 U的产生式的右边指出这个过程的代码结构: (1)若是终结符号,则和向前看符号对照, 若匹配则向前进一个符号;否则出错。 (2)若是非终结符号,则调用与此非终结符对应的过程。当A的右部有多个产生式时,可用选择结构实现。 三、实验要求 (一)准备: 1.阅读课本有关章节, 2.考虑好设计方案; 3.设计出模块结构、测试数据,初步编制好程序。 (二)上课上机: 将源代码拷贝到机上调试,发现错误,再修改完善。第二次上机调试通过。 (三)程序要求: 程序输入/输出示例: 对下列文法,用递归下降分析法对任意输入的符号串进行分析: (1)E->eBaA (2)A->a|bAcB (3)B->dEd|aC (4)C->e|dc 输出的格式如下: (1)递归下降分析程序,编制人:姓名,学号,班级 (2)输入一以#结束的符号串:在此位置输入符号串例如:eadeaa# (3)输出结果:eadeaa#为合法符号串 注意: 1.如果遇到错误的表达式,应输出错误提示信息(该信息越详细越好); 2.对学有余力的同学,可以详细的输出推导的过程,即详细列出每一步使用的产生式。 (四)程序思路 0.定义部分:定义常量、变量、数据结构。 1.初始化:从文件将输入符号串输入到字符缓冲区中。 2.利用递归下降分析法分析,对每个非终结符编写函数,在主函数中调用文法开始符号的函数。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花无凋零之时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值