一、实验目的
语法分析是编译程序中的核心部分。本实验通过设计一个典型的自顶向下语法分析程序——LL(1) 语法分析程序,进一步理解并掌握语法分析的原理和实现技术。
二、题目理解和说明
语法分析的主要任务是“组词成句”,将词法分析给出的单词序列按语法规则构成更大的语法单位,如“程序、语句、表达式”等;或者说,语法分析的作用是用来判断给定输入串是否为合乎文法的句子。
按照生成语法树的方向不同,常用的语法分析方法有两类:自顶向下分析和自底向上分析。自顶向下分析也称面向目标的分析方法,也就是从文法的开始符出发,试图推导出与输入单词串相匹配的句子。自底向上分析也称移进-归约分析方法,从输入单词串开始,试图归约到文法的开始符。
预测分析法(LL(1)方法)的基本思想是:从文法开始符S 出发,从左到右扫描源程序,每次通过向前查看 1 个字符,选择合适的产生式,生成句子的最左推导。
我所写的语法分析器是以C++语言为主。其开发环境是vs2017。
三、程序功能和框架
1.程序功能
(1)程序功能主要是:
(2)实现了输入终结符号集、非终结符号集、文法产生式集并进行存储。
(3)实现自动求解FIRST集以及FOLLOW集。
(4)实现根据FIRST集和FOLLOW集自动构建预测分析表M。
(5)实现对正确字串的分析识别,并记录识别过程最终显示在屏幕上。
(6)实现对错误字串的跳过识别,并记录识别过程最终显示在屏幕上。
2.程序框架
程序主要由三大模块组成。这三大块主要为:
(1)数据存储结构模块
数据存储结构模块主要是确立了终结符号集、非终结符号集、文法产生式集、输入测试串等必要输入数据的存储方式。
同时也进一步确立了FIRST集、FOLLOW集、预测分析表、预测分析栈以及相关集合元素数目记录变量的存储类型和方式。
(2)辅助函数模块
辅助函数模块主要是为了让核心函数模块的算法更容易实现,从而设计了一些辅助功能的函数。主要包括:分解带多选项的产生式(例:A->bc|RD)子程序、文法文字输入子程序、判断某符号元素是否在某相应集合中的子程序、集合相加子子程序、根据符号元素返回其所在存储结构下标子程序、去除集合重复元素子程序。
(3)核心函数模块
核心函数模块只包含四个子程序:FIRST集求解子程序、FOLLOW集求解子程序、预测分析表求解子程序、文法分析子程序。
这四类函数分别完成的功能是:求解FIRST集、求解FOLLOW集、构造预测分析表、对输入字串进行识别分析并输入分析步骤和分析结果。
四、设计过程
根据上面的程序框架分析,可以很容易得到,该语法分析器的设计过程分为三个主要过程。分别是:数据结构设计、辅助函数模块设计、核心算法设计。
1.数据结构设计
数据结构设计主要是对必要的输入数据和相关参数的存储类型和存储方式进行设计。主要有三类问题需要解决:输入数据、求解数据、相关参数。
(1)需要进行考虑的输入数据为:终结符号集、非终结符号集、文法产生式集、测试字串。
(2)需要进行考虑的相关求解数据为:非终结符号的FIRST集、非终结符号的FOLLOW集、预测分析表、分析栈。
(3)需要进行考虑的相关参数:终结符集的元素个数、非终结符号的元素个数、输入文法产生式个数、分解后的产生式个数。
针对第一类问题,测试字串的存储结构设计为全局变量的字符数组,容量为100,初始为空。终结符号集和非终结符号集都是全局变量的字符数组,容量为100,初始为空的存储设计。文法产生式集设计为二维字符数组全局变量,容量为100×100,初始为空。
具体设计为:
char word[100] = { "" };//待测试的文字
char Vt[100] = { "" };//终结符
char Vn[100] = { "" };//非终结符
string Generative[100] = { "" };//文法产生式存储
针对第二类问题,非终结符号的FIRST集和非终结符号的FOLLOW集设计为二维数组变量,容量为100×100,初始为空。分析栈设计为字符(char)类型的堆栈全局变量。预测分析表设计为二维整型数组,容量为100×100,初始化全为-1。
针对预测分析表进行说明:之所以把预测分析表设计成如此结构,是为了后面算法容易进行。该二维整型数组每个[i][j]元素存储的是第i个非终结符号和第j个终结符号所对应的产生式存储位置的下标。其中,i是非终结符号数组对应元素的下标,j同理。
具体设计为:
string first[100] = { "" };//first集合
string follow[100] = { "" };//follow集合
stack
int table[100][100] = { 0 };//预测表
针对第三类问题,所有的参数全部使用全局变量的整型变量进行存储。主要目的是为了后面算法进行循环时的方便。
具体设计:
int VtNum = 0;//终结符号的个数
int VnNum = 0;//非终结符号的个数
int GenNum = 0;//文法产生式个数
int GenNumNew = 0;//文法产生式分解后的个数
2.辅助函数模块设计
辅助函数模块设计不是本次实验程序设计的重点。本次程序设计的重点是下面的核心算法的设计。因此,本部分我只会大致提一下。
本部分主要包含六个主要函数。分别为:分解带多选项的产生式(例:A->bc|RD)子程序、文法文字输入子程序、判断某符号元素是否在某相应集合中的子程序、集合相加子子程序、根据符号元素返回其所在存储结构下标子程序、去除集合重复元素子程序。
(1)分解带多选项的产生式(例:A->bc|RD)子程序
该程序主要是为了后面构建预测分析表更方便而设计。主要功能是对输入在变量Generative中的原始文法产生式进行分解。原始文法产生式可能具有多选项产生式,如:C->+BC|@。该子程序会将这样的产生式分解为两个产生式:C->+BC、C->@。并将分解后的文法产生式扩展集合存储到新的一个变量中:GenerativeNew。该变量依然是二维字符数组全局变量,容量是100×100,初始为空。
其具体设计为:
string GenerativeNew[100] = { "" };//文法产生式分解后的存储
该子程序设计为:
void GramF();//分解产生式程序
具体实现较为简单,详见源码。
(2)文法文字输入子程序
该子程序分为两部分:文法输入和文字输入。文字输入即输入一个字符串实现简单不做介绍。文法输入包括:终结符集、非终结符集、文法产生式输入三个部分。前两部分是输入的字符串,后一部分输入的是n多条字符串,以”end”结尾。
需要注意:输入的终结符号必须是全部,不需要输入’#’。输入的非终结符号必须开头字符是开始符号。本语法分析器以’@’符号作为空符号。
具体设计:
void GramIn();//文法输入程序
void WordIn();//文字输入程序
详细实现见源码。
(3)判断某符号元素是否在某相应集合中的子程序
该部分分为两个函数:判断某符号元素在终结符集还是在非终结符集、判断某符号是否在某个FIRST集或者FOLLOW集。
前者输入参数是:某字符(char)、需要判断的字符集合(char [])、字符集合容量(int)
输出结果:是或否。
具体设计:
bool Is(char x, char a[], int n); //是否在集合中
后者输入参数是:某字符(char)、需要判断的字符集合(char [])
输出结果为:是或否。
bool IsChar(char a, string b);//判断一个字符是否在一个集合中的程序
详细实现见源码。
(4)集合相加子子程序
集合相加主要用在求解FIRST集和FOLLOW集的过程中。
其输入是:需要扩展的字符集(string&)、被加入的字符集(string&)
输出:无
具体设计为:
void ADD(string& a, string& b);//集合相加程序
void ADDfollow(string& a, string& b);//集合(follow)相加程序
上述两个函数的区别在于,前者需要去掉’@’空字符。后者不需要。
详细见源码。
(5)根据符号元素返回其所在存储结构下标子程序
该子程序用处较多。在构建预测分析表、分析字串识别程序、求解FIRST和FOLLOW集的算法函数中都有所涉及。
输入是:符号元素(char)
输出:该元素所在村粗结构位置下标
具体设计为:
int Back(char a);//根据字母返回下标程序
详细见源码。
(6)去除集合重复元素子程序
在求解FIRST集和FOLLOW集后,需要对相应集合的重复元素去除和整理,这样才能为后面正确求解预测分析表做好基础工作。
输入:无
输出:无。该函数直接对FISRT和FOLLOW集直接整理。
具体设计为:
void clearF();//净化程序
详细见源码。
3.核心算法设计
核心算法主要包括四个部分:FIRST集求解子程序、FOLLOW集求解子程序、预测分析表求解子程序、文法分析子程序。该部分是整个语法分析器的核心部分,我会作为重点进行介绍。
(1)FIRST集求解子程序
FIRST集求解主要依据原理为:
对每一文法符号XÎ(VNUVT)*
(1)若XÎVT ,则FIRST(X)={X}。
(2)若XÎVN ,且有产生式X®a¼,aÎVT,则aÎFIRST(X)。
(3)若XÎVN ,X®e,则eÎFIRST(X)
(4)若XÎVN,Y1,Y2,¼,Yi ÎVN,
且有产生式X®Y1,¼,Yn。
若对于1≤i≤k≤n,都有YiÞe, 则FIRST(Yk+1)-{e} FIRST(X)
算法描述为:
对某个输入的待求解字符A做如下操作:
如果产生式右部第一个字符为终结符,则将其计入左部first集
如果产生式右部第一个字符为非终结符执行以下步骤
求该非终结符的first集
将该非终结符的非空first集计入左部的first集
如果产生式左部是空,则直接将空计入左部Afirst集。
其程序流程图设计为:
根据流程图进行编程,其具体实现如下:
/************first集求解程序*************/
void GetFIRST(char a) {
int i, k = 0;
for (i = 0; i < GenNumNew; i++) {
if (GenerativeNew[i][0] != a)
continue;
if (Is(GenerativeNew[i][3], Vt, VtNum)) {
//如果该非终结符产生式右部第一个字符是终结符号
//则直接将其计入左部非终结符的FIRST集
first[Back(GenerativeNew[i][0])] += GenerativeNew[i][3];
}
else if (Is(GenerativeNew[i][3], Vn, VnNum)) {
//如果该非终结符号右部第一个字符是非终结符号
//则对该右部第一个字符的FIRST进行求解
//并将其加入左部字符的FIRST集
GetFIRST(GenerativeNew[i][3]);
ADD(first[Back(GenerativeNew[i][0])], first[Back(GenerativeNew[i][3])]);
}
else if (GenerativeNew[i][3] == '@') {
//如果该非终结符产生式是个空
//则将空加入左部字符的FIRST集
int j = 0;
while (first[Back(GenerativeNew[i][0])][j] != '\0') {
if (first[Back(GenerativeNew[i][0])][j] == '@') {
k = 1;
break;
}
j++;
}
if (!k)
first[Back(GenerativeNew[i][0])] += '@';
}
}
}
/************first集求解程序*************/
从上面的函数设计可以看出,这是个递归函数。在求解右部第一个非终结符的FIRST集时调用了自己。i的作用是对分解后的产生式集合进行遍历。由于有些非终结符具有多个选项,因此分解后,该字符分布在不同的产生式中。如:C->+BC|@分解为:C->+BC和C->@,那么对C字符求解时,需要对上面两个产生式分别求解。因此需要遍历。
(2)FOLLOW集求解子程序
FOLLOW求解的原理如下:
其算法描述为:
对于文法G中每个非终结符A构造FOLLOW(A)的办法是,连续使用下面的规则,直到每个FOLLOW不在增大为止.对于文法的开始符号S,置#于FOLLOW(S)中;若A->aBb是一个产生式,则把FIRST(b)\{ε}加至FOLLOW(B)中;若A->aB是一个产生式,或A->aBb是一个产生式而b=>ε(即ε∈FIRST(b))则把FOLLOW(A)加至FOLLOW(B)中
其流程图设计为:
根据流程图设计函数的具体实现:
/************follow集求解程序*************/
void GetFOLLOW(char a) {
int nk;
nk = Back(a);
int i, j;
i = nk;
if (i == 0) {
//如果待求解字符是开始字符
//则把'#'加入其FOLLOW集
if (IsChar('#', follow[Back(a)]))
;
else
follow[Back(a)] += '#';
}
for (j = 0; j < GenNumNew; j++) {
if (GenerativeNew[j][3] == a && GenerativeNew[j][4] != '\0') {//如果是A->Bb
if (Is(GenerativeNew[j][4], Vt, VtNum)) {
//如果b是终结符号,直接加入follow(B)
if (IsChar(GenerativeNew[j][4], follow[Back(a)]))
;
else
follow[Back(a)] += GenerativeNew[j][4];
}
else if (Is(GenerativeNew[j][4], Vn, VnNum)) {
//如果b是非终结符号,需要判断
if (IsChar('@', first[Back(GenerativeNew[j][4])])) {
//如果b可以推出空'@',则需要将follow(A)加入follow(B)
GetFOLLOW(GenerativeNew[j][0]);
ADDfollow(follow[Back(a)], follow[Back(GenerativeNew[j][0])]);
}
ADD(follow[Back(a)], first[Back(GenerativeNew[j][4])]);
}
}
else if (GenerativeNew[j][4] == a && GenerativeNew[j][5] != '\0') {//如果是A->aBb
if (Is(GenerativeNew[j][5], Vt, VtNum)) {
//如果b是终结符号,直接加入follow(B)
if (IsChar(GenerativeNew[j][5], follow[Back(a)]))
;
else
follow[Back(a)] += GenerativeNew[j][5];
}
else if (Is(GenerativeNew[j][5], Vn, VnNum)) {
//如果b是非终结符号,需进行判断
if (IsChar('@', first[Back(GenerativeNew[j][5])])) {
//如果b可以推出空'@',则需要将follow(A)加入follow(B)
GetFOLLOW(GenerativeNew[j][0]);
ADDfollow(follow[Back(a)], follow[Back(GenerativeNew[j][0])]);
}
ADD(follow[Back(a)], first[Back(GenerativeNew[j][5])]);
}
}
else if (GenerativeNew[j][4] == a && GenerativeNew[j][5] == '\0') {//如果是A->aB
GetFOLLOW(GenerativeNew[j][0]);//直接将follow(A)加入follow(B)
ADDfollow(follow[Back(a)], follow[Back(GenerativeNew[j][0])]);
}
}
}
/************follow集求解程序*************/
上输入求解过程中需要对分解后的产生式集进行遍历求解。这样会对全部的非终结符无遗漏的求解完成。
(3)预测分析表求解子程序
预测分析表的求解原理:
算法语言描述为:
对文法G的每个产生式A->a执行第二步和第三步;
对每个终结符a∈FIRST(a),把A->a加至M[A,a]中;
若ε∈FIRST(a),则把任何b∈FOLLOW(A)把A->a加至M[A,b]中;
把所有无定义的M[A,a]标上出错标志.
其算法流程图为:
其函数具体实现如下:
/************预测分析表构建程序*************/
void FAtable() {
int i, j, k;
for (i = 0; i < VtNum; i++) {
for (j = 0; j < GenNumNew; j++) {
if (Vt[i] == GenerativeNew[j][3])
//如果终结符Vt[i]在A->a的first(a)中,则将A->a放入table[A,Vt[i]]中
table[Back(GenerativeNew[j][0])][i] = j;
else if (Is(GenerativeNew[j][3], Vn, VnNum)) {
if (IsChar(Vt[i], first[Back(GenerativeNew[j][3])])) {
table[Back(GenerativeNew[j][0])][i] = j;
}
}
else if (GenerativeNew[j][3] == '@') {
//如果当前的产生式是:A->a且,a='@',则判断当前的Vt[i]是否在
if (IsChar(Vt[i], follow[Back(GenerativeNew[j][0])])) {
table[Back(GenerativeNew[j][0])][i] = j;
}
}
}
}
}
/************预测分析表构建程序*************/
(4)文法分析子程序
文法分析原理:
算法语言描述为:
预测分析程序的总控程序在任何时候都是按STACK栈顶符号X和当前的输入符号行事的,对于任何(X,a),总控程序 每次都执行下述三种可能的动作之一;若X=a=”#”,则宣布分析成功,停止分析过程.若X=a≠”#”,则把X从STACK栈顶逐出,让a指向下一个输入符号.若X是一个非终结符,则查看分析表M,若M[A,a]中存放着关于X的一个产生式,那么,首先把X逐出STACK栈顶,然后把产生式的右部符号串按反序一一推进STACK栈(若右部符号为ε,则意味着不推什么东西进栈).在把产生式的右部符号推进栈的同时应做这个产生式相应得语义动作,若M[A,a]中存放着”出错标志”,则调用出错诊察程序ERROR.
文法分析流程图:
函数具体实现为:
/************文法分析程序*************/
void GAnalysis() {
int i = 0, x, y, k,error=0,n=1;
char abc;
string chan = "";
st.push('#');
st.push(Vn[0]);
abc = st.top();
while (!(abc == word[i] && abc == '#')) {
if (Is(st.top(), Vn, VnNum)) {
x = Back(st.top());
y = BBack(word[i]);
k = table[x][y];//获得产生式
if (k == -1) {
error++;
cout << "步骤["< n++; i++; //break; } else { chan = GenerativeNew[k]; k = 0; st.pop(); while (chan[k] != '\0') { k++; } k--; if (chan[k] != '@') { while (chan[k] != '>') { st.push(chan[k]); k--; } //i++; cout << "步骤[" << n << "]:用" << chan << "的右部分逆序入栈已经完成;\n"; n++; } else { cout << "步骤[" << n << "]:用" << chan << ";\n"; n++; //i++; } } } else if (Is(st.top(), Vt, VtNum)) { if (st.top() == word[i]) { cout << "步骤[" << n << "]:匹配栈顶和当前符号" << word[i] << ",成功;\n"; st.pop(); i++; n++; } else { cout << "步骤[" << n << "]:识别失败!!\n"; n++; break; } } abc = st.top(); } if (error) { cout << "步骤[" << n << "]:识别错误!!错误跳过次数:"< n++; } else { cout << "步骤[" << n << "]:识别成功!!\n"; n++; } } /************文法分析程序*************/ 用户可以在相应的vs编译环境或者devc++环境上运行此程序。 用户运行此程序时,需要先输入终结符号集,然后输入非终结符号。且,非终结符号第一个字符一定是文法的开始符。最后分步骤输入文法产生式,最后以”end”作为输入结束标志。 在运行后,会在屏幕上显示输出FIRST集和FOLLOW集以及预测分析表。 然后会提示用户输入测试字串,测试字串最后一个字符一定是’#’。五、用户操作指南