编译原理实验报告
目 录
目录
1 实验目的
(1) 掌握语法分析过程,即语法分析是如何根据语法规则逐一分析词法分析时得到的属性字,检查语法错误。
(2) 构造LR分析程序,利用它进行语法分析,判断给出的符号串是否为该文法识别的句子。
(3) 理解LR分析方法是严格的从左向右扫描,和自底向上的语法分析方法。
(4) 理解词法分析和语法分析的接口方式。
2 实验内容
完善TPL语言文法,然后手动或者编程创建LR分析表,然后编写TPL语言的语法分析程序,它从左到右依次对词法分析得到的结果数组或链表等存储结构中进行读取,判断是否符合语法规则,并给出语法分析过程和结果。
3 实验要求
(1) 根据TPL语言的文法规则建立LR分析表;
项目集、项目集之间的转换函数、LR分析表的构造过程,要求写程序实现。该程序的输入是文法的所有产生式,输出是LR分析表。程序尽可能通用,即输入的文法改变了,那么也可以给出对应的LR分析表。
有困难的同学,也可以手动构造LR分析表。
(2) 采用自底向上方法来设计TPL语法分析程序,实现以下功能:
① 能对任何TPL语言词法分析结果进行语法分析。
② 输出分析过程。
③ 若语法分析过程有错误,则定位错误,并指出错误原因。
4 实验原理
LR(Left-to-Right,Rightmost derivation)语法分析是一种常用的自底向上(bottom-up)的语法分析方法。LR分析器的核心思想是从输入串的左端推导出文法的开始符号,同时使用一个栈来保存已经识别的部分。LR分析器通过一系列的移进(Shift)和归约(Reduce)操作来构建分析树,并最终判断输入串是否符合文法。ACTION和GOTO表:LR分析表包括两个部分,一个是ACTION表,另一个是GOTO表。ACTION表用于处理终结符的移进和归约操作,而GOTO表用于处理非终结符的转移。
ACTION表中的项可以是移进操作、归约操作或接受操作。移进操作表示将输入符号移到栈中,归约操作表示使用产生式进行规约,接受操作表示输入串符合文法规则。
GOTO表用于在分析栈中进行非终结符的状态转移。
分析栈和输入缓冲区:LR语法分析使用一个分析栈来保存已经识别的部分,同时从输入缓冲区读入符号。根据ACTION和GOTO表的信息,分析器进行移进、归约等操作,直到接受或出错。
LR(1)语法分析:为了处理具有相同前缀的产生式,LR(1)语法分析引入了“查看符号”(Lookahead Symbol)的概念。LR(1)项目集包含了在某个位置上的项目以及紧随其后的一个终结符,使得分析器可以根据更多的信息进行决策。
4.1 TPL语言语法分析的文法
据程序设计语言的语法规则,描述包含赋值语句、控制语句的源程序的语法,TPL 语言对应的文法产生式P->A
A->BCH
A->BCD
A->BCG
B->39
C->86 56 87 78 C
C->86 56 87 79
D->17 71 E 72 H
D->17 71 E 72 G
D->17 71 E 72 F
G->34 71 E 72 F
G->34 71 E 72 H
G->34 71 E 72 D
E->86 I 87
F->86 56 87 79
F->86 56 86 J
H->15 71 B 86 56 87 79 E 79 86 45 72 75 D 76
H->15 71 B 86 56 87 79 E 79 86 45 72 D
H->15 71 B 86 56 87 79 E 79 86 45 72 75 H 76
H->15 71 B 86 56 87 79 E 79 86 45 72 H
H->15 71 B 86 56 87 79 E 79 86 45 72 75 G 76
H->15 71 B 86 56 87 79 E 79 86 45 72 G
H->15 71 B 86 56 87 79 E 79 86 45 72 F
I->50|47
J->40 87 J
J->40 86 J
J->40 87 79
J->40 86 79
4.2
设计思路
1、因为文法是自己设置的,所以可以自己规定文法的结构,所以规定,开始符的规则一定只有一条,所以不用补充开始符。因此所有非终结符就可以设置为一位的,就不用开二维数组来保存非终结符了。同时,因为用的VS编译器不支持输出ASCII码之外的阿拉伯符号,因此我用->表示规则中的箭头,.表示项目集中的点。
2、因为要对程序进行分析,然后词法分析器输出的是单词表中的种别码,所以,设置终结符全部为数字。因为语法分析器的输入应该是词法分析器的输出,所以,用一个全局一维数组来将词法分析输出的每个种别码保存,并记录个数,作为语法分析器的输入串。同时,因为将种别码对应的单词(关键字、标识符、运算符、界符、整型和浮点型)输出格式不好看,所以我直接对种别码进行分析,文法也是用种别码来当作终结符构造的。
- LR(1)文法的难度主要是在如何构造项目集和分析表。我创建了一个结构体来保存并构建项目集。项目集和项目集的闭包都是用模拟算法求的,也就是直接模拟做题时的步骤将算法写出。同时,为了构造分析表,在每个项目集求完闭包之后,要将项目集与下一个项目集的联系保存。为了保存项目之间的联系,用pair定义了两个新的二元组类型CI和PII。
因为非终结符是单个大写字母,而终结符是整型数字,所以创建了两个二元组来分别保存,二元组第一个元素表示的时非终结符或终结符,第二个元素则表示下一个项目集的编号。
因为LR(1)分析法求闭包需要求向前搜索符,所以我单独写了个求first集的函数,但是这个求first的函数仅求一个终结符并且不能求太复杂的规则的first。此外,因为在求项目集时要判断点后面的字符是非终结符还是终结符,因此还写了判断点后面是否是非终结符以及求点后面的非终结符和终结符的函数。还写了规则中点移动的函数,因为保存项目集的结构体是定义为全局变量的,所以如果直接将规则传入函数的话,会将项目集中的规则改变,所以定义了一个临时变量来进行移动,点移动之后在将临时变量的值赋给下一个项目集的规则
4.3LR分析表
5. 数据结构、函数说明和主要算法
5.1 主要的数据结构
文法规则结构体(GS):
cpp
Copy code
struct GS {
char vn[10]; // 非终结符数组
int nvn; // 非终结符个数
int vt[10]; // 终结符数组
int nvt; // 终结符个数
char gs[50][50]; // 扩展后的文法数组
int ngs; // 扩展后的文法个数
} gs;
项目集结构体(DFANode):
cpp
Copy code
struct IGS {
char igs[50]; // 文法规则
int x; // 向前搜索符
};
struct DFANode {
IGS gather[50]; // 项目集中的规则
int sum; // 项目集中的规则数量
int i; // 项目集编号
// 下面是与其他项目集的联系
pair<char, int> nextci[50]; // 与非终结符的联系
pair<int, int> nextpii[50]; // 与终结符的联系
int nci; // 非终结符联系的个数
int npii; // 终结符联系的个数
} dfa[50];
LR(1)分析表项结构体(TableItem):
cpp
Copy code
struct TableItem {
int i; // 状态编号
pair<char, int> ci[50]; // 非终结符与下一个状态的关系
pair<int, int> pii[50]; // 终结符与下一个状态的关系
int nci; // 非终结符联系的个数
int npii; // 终结符联系的个数
} table[50];
5.2 所有函数的说明
void open_file():
打开文件并读取文法规则,将文法规则扩展,生成 gs 结构体。
识别非终结符和终结符,并对终结符进行排序。
void print_gs():
输出从文件读入并扩展后的文法,以及非终结符和终结符的集合。
void print_dfa():
输出 LR(1) 项目集规范族(DFA)的各个项目集及项目之间的联系。
void print_table():
输出由 DFA 构建的 LR(1) 分析表,包括 ACTION 和 GOTO 表。
void analysis_table():
生成分析表,填充 ACTION 和 GOTO 表。
void input_analysis():
对输入串进行 LR(1) 分析,输出每一步的状态栈、符号栈、输入串、产生式、ACTION、GOTO 等信息。
char* spcode_to_character(int n, char s[]):
将种别码转换为对应的关键字、分界符或运算符字符串。
add_vn(char c), add_vt(int n): 将非终结符和终结符添加到相应集合中。
pd_integer(char c): 判断字符是否为数字。
pd_character2(char c): 判断字符是否为非终结符。
pd_invn(char c), pd_invt(int n): 判断是否为非终结符或终结符的补充规则。
pd_pafter(char c): 判断规则后面是否为非终结符。
get_rule(char s[]): 获取输入的规则在文法中的位置。
5.3 主要算法
DFA 构建算法:
void build_dfa() {
initialize(); // 初始化一些数据结构
int i = 0;
while (i < ni) {
// 创建新的项目集
create_new_item_set(i);
// 对项目集中的每个规则进行处理
int k = 0;
while (k < dfa[now_i].sum) {
process_item(dfa[now_i].gather[k]);
k++;
}
// 查重,合并相同的项目集
handle_duplicates();
i++;
}
}
void process_item(IGS item) {
// 处理项目集中的每个规则,更新dfa、table等数据结构
// ...
}
void create_new_item_set(int i) {
// 创建新的项目集,并处理其中的规则
// ...
}
void handle_duplicates() {
// 处理重复的项目集,合并相同的项目集
// ...
}
LR(1) 分析算法:
void input_analysis() {
int n = nsp, s[N] = { 0 };
// 初始化输入串
stack<int> stack_state;
stack<int> stack_symbol;
while (i < n) {
// 从分析表中获取动作和产生式
int k = stack_state.top();
int np = get_action_or_goto(k, s[i]);
if (np == 666) {
// acc,成功接收
break;
}
if (is_shift(np)) {
// 移进操作
shift_operation(np);
i++;
} else {
// 归约操作
reduce_operation(np);
}
step++;
}
}
int get_action_or_goto(int state, int symbol) {
// 从分析表中获取 ACTION 或 GOTO 值
// ...
}
void shift_operation(int np) {
// 处理移进操作
// ...
}
void reduce_operation(int np) {
// 处理归约操作
// ...
}
这两个算法分别描述了LR(1)分析器的两个关键步骤:DFA的构建和LR(1)分析。在DFA的构建中,通过不断处理项目集和规则,构建LR(1)项目集规范族。在LR(1)分析中,通过分析表,不断进行移进和归约操作,最终完成对输入串的分析。这两个算法是LR(1)分析器的核心逻辑。
6. 实现与测试
6.1 源程序1测试
7 实验总结
持续两个周的编译原理课程设计,让我学到了很多东西。首先是加深了对编译原理课程的理解,对词法分析、语法分析了有了更进一步的掌握,其次是编程能力的提高,这样的代码量,一年也只有几次能够遇到。有了前面课程设计的经验,这一次的编译原理课程设计,我更注重数据结构,数据结构的好坏直接决定了代码的复杂度,代码的量,调试困难,遇到错误常常会使程序直接崩溃。
我对一个编译器的组成有了更深层次的认识。通过本次的课程设计,我对自动机有了更深入的了解,也希望这些理论会再我们日后的学习中发挥更大的作用
8 参考文献
[1] 张素琴,吕映芝,蒋维杜等.编译原理[M].北京:清华大学出版社,2015.
[2] 张敬敏,李霞.编译原理实验指导书[Z].石家庄,2021.
指导教师评语 |
指导教师签字
|