当初写的时候,用了相对路径结果出了点bug,为了偷懒写的绝对路径-_-||,在使用时需要将修改为自己的路径
一、题目要求
- 左递归判定:输入CFG,判定是否含有左递归及左递归的类型(直接左递归还是间接左递归)
- 直接左递归的消除:输出消除直接左递归后的新文法
- 间接左递归的消除:输出消除间接左递归后的新文法
- 可视化界面:结果的可视化展示
二、系统分析
① 打开文件功能。点击打开文件按钮,弹出文件选择对话框,选择文件并文件中的内容输出到输入CFG区域。
② 过滤CFG空格功能。如果输入的产生式中有空格,过滤。
③ 获取开始符号功能。获取所有产生式的开始符号。
④ 获取候选式功能。获取所有产生式的候选式,按产生式的顺序储存在二维数组中。
⑤ 左递归判定功能。判断是否有左递归,及什么类型的左递归。
⑥ 直接左递归的消除功能。
⑦ 间接左递归的消除功能。
⑧ 保存文件功能。点击保存文件按钮,将输出区域的内容保存至指定文件夹。
⑨ 清除输入及复制新文法功能。点击清除按钮将输入区域的内容清除,点击复制按钮将输出区域的内容复制到输入区域。
⑩ 可视化界面功能。
三、系统设计
1、系统功能结构
文法左递归消除的功能结构如图所示
文法左递归结构分为前台和后台部分。前台包括CFG输入、新文法输出、开始以及退出功能;后台部分包括获取CFG以及开始符号和候选式、判断递归类型、消除直接左递归和消除间接左递归。
2、系统流程图
文法左递归消除的主流程图设计如图所示:
选择手动输入CFG文法或者点击打开文件选择相应的txt文件获取CFG文法,将CFG文法输出在输入区域,若输入错误,可选择清除按钮将输入区域的内容清除;
点击开始按钮进入左递归文法的判断及消除,先将文法中的空格去掉,提取开始符号及个数,获取所有的候选式,再判断文法的递归类型,消除对应类型的左递归并输出新文法,若不为左递归则输出原文法;
点击退出按钮则退出整个程序;
点击保存按钮则将输出的结果保存至指定文件中;
点击复制按钮则将生成的新文法复制到输入区域。
四、系统详细设计
1、主窗口类(模块)的详细设计
(1)start槽函数的程序流程图
对流程图的描述:
- 从文本输入框中获取输入的文法并转化为字符数组形式,过滤文法中的空格方便对文法进行相关处理,并获取文法中的开始字符及其个数。然后获取候选式,从文法的起始位置,对开始字符进行循环,因为‘>’与‘|’后的字符串即为候选式,故当碰见‘>’或‘|’时,文法后移进入候选式,当当前文法字符不为‘|’、换行和数组结尾时,将当前字符加入候选式中,文法后移,直到为‘|’、换行或数组结尾时退出当前候选式的循环。
- 退出循环后判断当前文法字符为换行或结尾,若是则退出当前开始符号的循环,处理下一个开始字符的候选式;若当前文法字符为‘|’则当前产生式还有候选式为处理,候选式计数器加1,将当前候选式内容加入候选式中,直到开始符号处理完毕。
(2)主要代码
1. 获取候选式
/* -------------------------- 获取候选式 --------------------------------------------*/
// 将候选式用QString二维数组储存
QString candidateStr[100][100];
int i = 0; // 计数cfg文法
for(int j = 0;j < countSymbol;j++){
int k = 0;
while(true){
// '>'和'|'后即为候选式
if('>' == cfg_char[i] || '|' == cfg_char[i]){
i++; // 跳过'>'和'|',进入候选式
// 未遇到'|'、换行、结尾就将其加入候选式中
while('|' != cfg_char[i] && '\n' != cfg_char[i] && '\0' != cfg_char[i]){
candidateStr[j][k] += cfg_char[i];
i++;
}
// 当碰到换行时退出while循环,进入下一个开始符号的候选式;碰见数组结尾时退出while循环
if('\n' == cfg_char[i] || '\0' == cfg_char[i])
break;
else
k++; // 当前开始字符的候选式后移
}
else
i++;
}
}
2. 打开文件
// 打开文件
void MainWindow::openFile(){
QString filePath = QFileDialog::getOpenFileName(this,"选择文件","E:\\Programing\\QT\\LeftRecursionRemoval\\LeftRecursionRemoval\\grammar\\cfg文法","(*.txt)");
if(filePath.isEmpty()){
return;
}
QFile file(filePath);
file.open(QIODevice::ReadOnly); // 以只读方式打开
QByteArray content = file.readAll();
ui->textEdit->setText(content); // 显示数据
file.close();
}
3. 保存文件
// 储存文件
void MainWindow::storeFile()
{
QString filePath = QFileDialog::getSaveFileName(this,"保存文件", "E:\\Programing\\QT\\LeftRecursionRemoval\\LeftRecursionRemoval\\grammar\\result","(*.txt)");
if(filePath.isEmpty()){
return;
}
QString grammar = newGrammar;
QFile file(filePath);
if(!file.open(QIODevice::ReadWrite | QIODevice::Truncate)){ // 文件不存在则创建,覆盖写入
QMessageBox::warning(this,"ERROR","打开文件失败,数据保存失败");
return;
}
file.write(grammar.toLatin1());
file.close();
}
2、间接左递归类(模块)的详细设计
(1)判断间接左递归及消除间接左递归生成新文法的函数流程图
流程图的描述:
- 将候选式数组保存,避免后续操作会破坏原始候选式数组,影响二次调用。用retain数组标记产生式,判断是否保留;将开始符号按从下到上进行循环,Uj记倒数第一个为起始位置j,Ui记倒数第二个为起始位置i,并初始化数据用来保存相关信息,如数组a用来保存α,数组b用来保存β等。对临时候选式进行循环,若当前Ui的候选式的第一个字符等于Uj,则将当前候选式除掉Uj并存入数组a中,再将have(判断Ui的第一位是否为Uj)置为true,将retain数组在位置j处的值置1,即标记为丢弃;若候选式的第一个字符不为Uj,则获取Ui当前的候选式存入数组b中。
- 当前产生式的候选式处理完毕后判断当前Ui候选式时候含有Uj,若有则将Uj的候选式β与α进行组合,再加上不包含Uj的候选式,得到新的候选式,并在对应位置覆盖掉临时候选式数组。
- 获取新候选式后或Ui候选式不包含Uj时,判断Uj是否到达Ui的下一个,若是则将Ui的计数器减1,Uj的计数器重置为倒数第一个;若没有则将Uj的计数器减1,Ui的计数器不变。判断Ui的计数器是否小于0时,若是则退出循环,不是则继续循环。
- 上面将候选式中的开始符号替换,将当前产生式的开始字符与所有候选式的第一个字符进行比较,若相等则跳出循环,并标识为间接左递归;若不相等则继续循环直到产生式处理完毕。最后将候选式进行格式处理,生成新文法。
(2)主要代码
消除间接左递归
/* ------------------------------- 消除间接左递归 ----------------------------------------- */
int retain[100] = {0}; // 需要一个数组来记录哪些产生式丢弃
int j = countSymbol-1; // Uj,从倒数第一个起
for(int i = countSymbol-2;i >= 0;){ // Ui,从开始符号的倒是第二个起,数组从0开始,故减2
QString a[100]; // a长的像α,用来储存α
QString b[100]; // a存α,b存不包含Uj的候选式β
bool have = false; // 判断Ui候选式的第一位是否为Uj
int a_count = 0,b_count = 0; // a_count对α进行计数,b_count对b进行计数
// 循环Ui的候选式,查找是否含有Uj
for(int m = 0;'\0' != temp_candidateStr[i][m];m++){
// 用a保存含Uj的α
if(temp_candidateStr[i][m].left(1) == startSymbol[j]){ // 候选式的第一位与开始符号相比
QString temp = temp_candidateStr[i][m]; // 保留候选式,防止候选式被破坏
a[a_count] = temp.remove(startSymbol[j]); // 获取α
a_count++;
have = true; // 包含Uj,have置为true
retain[j] = 1; // Ui中找到Uj,置1,丢弃Uj这个产生式
}
// 用b保存不含Uj的候选式
else{
b[b_count] = temp_candidateStr[i][m];
b_count++;
}
}
// 如果在Ui的候选式中找到Uj,就将Ui的α与Uj的候选式组合,并赋值给candidateStr
QString candidate[100]; // 保存β+α+“不包含Uj的候选式”
int k = 0; // 计数candidate
// Ui候选式第一个字符是否为Uj
if(have){
// 组合β与α
for(int p = 0;temp_candidateStr[j][p]!='\0';p++){ // Uj,β
for(int q = 0;a[q] != '\0';q++){ // Ui,α
candidate[k] = temp_candidateStr[j][p] + a[q]; // β+α
k++;
}
}
// 加上不包含Uj的候选式
for(int p = 0;b[p] != '\0';p++){
candidate[k] = b[p];
k++;
}
// 将组合后的候选式赋值给Ui,覆盖掉原候选式
for(int p = 0;candidate[p] != '\0';p++){
temp_candidateStr[i][p] = candidate[p];
}
}
// 当i、j相差1时,即Uj为Ui的下一个时,Ui向上走,Ui-1,Uj重新回到最后
if(i == j-1){
i--;
j = countSymbol-1;
}
// Uj未到Ui下一个时,Uj向上移
else
j--;
}
3、过滤提取类(模块)的详细设计
(1)提取开始符号函数的程序流程图
流程图的描述:
- 初始化文法计数器i,判断文法是否结束,若结束则返回储存开始字符的字符串数组;若文法未结束,先初始化保存开始字符的字符串。
- 如果文法计数器i的值为0,当当前文法字符为“-”且下一个字符为“>”时,判断数组是否达到结尾,若到达则退出循环,若没有则将当前获取的字符串加入开始字符串数组中,开始字符计数器加1,跳出循环,文法计数器加1,回到判断文法是否结束处;若不为“-”和“>”时,则将当前字符添加到临时开始字符串中,文法后移继续寻找“-”和“>”;
- 若计数器i的值不为0时,判断当前文法的字符是否为换行,若不为则直接返回;若为换行,判断文法数组是否到达结尾,若达到则退出循环;若没有则将文法计数器加1跳过换行字符,再判断文法是否处于“->”的“-”处,若是则将当前获取的字符串加入开始字符串数组中,开始字符个数加1,跳出循环,文法计数器加1,回到判断文法是否结束处;若不是则将当前字符添加到临时开始字符串中,文法后移继续寻找“-”和“>”。
(2)主要代码
提取开始字符
// 提取开始字符
QString* Filter::filterStartSymbol(char *cfg_char){
for(int i = 0;'\0' != cfg_char[i];i++){
QString symbol = "";
// 将第一行单独拿出,文法开始处即为开始符号
if(0 == i){
// 如果开始字符的起始位置前有空行则跳过
while('\n' == cfg_char[i]){
if('\0' == cfg_char[i])
break;
i++;
}
while(true){
// 若到达结束则直接退出
if('\0' == cfg_char[i])
break;
// 即遇到'->'代表开始符号结束,就将前面获取的字符串赋值给开始符号
if('-' == cfg_char[i] && '>' == cfg_char[i+1]){
startSymbol[countSymbol] = symbol;
countSymbol++;
break;
}
symbol += cfg_char[i]; // 有可能遇见两个字符的开始符号,即A'
i++;
}
}
// 其余行情况
else if('\n' == cfg_char[i]){ // 换行,下一行开始即为开始符号
i++;
while(true){
// 若到达结束则直接退出
if('\0' == cfg_char[i])
break;
// 即遇到'->'代表开始符号结束,就将前面获取的字符串赋值给开始符号
if('-' == cfg_char[i] && '>' == cfg_char[i+1]){
startSymbol[countSymbol] = symbol;
countSymbol++;
break;
}
symbol += cfg_char[i];
i++;
}
}
}
return startSymbol;
}
4、判断递归类(模块)的详细设计
(1)判断文法递归类型的程序流程图
流程图的描述:
- 初始化开始字符计数器i,判断是否将开始符号处理完毕,若没有则初始化候选式的计数器j,判断当前产生式是否结束,若结束则文法计数器i加1,继续循环;若未结束则判断当前产生式的开始字符是否等于候选式的第一个字符,若是则返回“direct”,若不是则候选式计数器加1,继续循环;
- 若开始符号处理完毕则判断是否为间接左递归,调用函数获取判断是否为间接左递归的bool值,若bool值为true则返回“indirect”,若不为true则返回“neither”。
(2)主要代码
判断直接左递归
/* -------------------------- 判断直接左递归 ------------------------*/
// 当候选式的第一个字符为对应行的开始字符时,即为直接左递归
for(int i = 0;i < countSymbol;i++){
int j = 0;
while("\0" != candidateStr[i][j]){
if(candidateStr[i][j].left(1) == startSymbol[i]) // 判断候选式的第一个字符是否为开始符号
return "direct";
j++;
}
}
5、消除直接左递归生成新文法类(模块)的详细设计
(1)消除直接左递归生成新文法的程序流程图
流程图的描述:
- 初始化新文法为空,初始化开始字符计数器i,若产生式处理完毕则退出,若未处理完则继续对当前产生式的候选式进行循环。
- 处理当前产生式的所有候选式,判断是否有第一个字符为当前开始字符时,则若则标记为true并退出循环;若没有则继续循环,直到候选式处理完毕。
- 对输出进行格式处理生成新文法,将为空的第一行first及第二行second赋值为开始字符+“->”;然后对候选式进行处理,若标记的值为true,在处理不含开始字符的候选式时,在first后加上候选式+开始字符+“’|”,在处理含开始字符的候选式时,在second后加上去掉开始字符的候选式+开始字符+“’|”。若判断开始符号的值为false则在first后加上候选式+“’|”,将second行置为空;将所有候选式处理完后去掉first行多余的“|”,如果标记的值为true,则在第一行加上换行,第二行加上“~”代表空;然后更新新文法为新文法+first行+second行+换行,将所有产生式处理完毕后退出循环后返回新文法。
(2)主要代码
消除直接左递归
/*
* 直接改写法
* U->Ux|y
* U->yU' ------------ first
* U'->xU'|~ --------- second
* 1、先遍历判断候选式是否含有开始符号
* 2、若有,则这个产生式为直接左递归,将输出结果分为两行,第一行存储包含开始符号的结果,第二行存储不包含开始符号的内容
* 3、若没有,则这个产生式不为直接左递归,直接输出
*/
QString newGrammar = "";
bool jugeSymbol; // 判断候选式是否有开始符号
for(int i = 0;i < countSymbol;i++){
// 判断此产生式的候选式的第一个字符是否为开始字符
jugeSymbol = false;
for(int j = 0;'\0' != candidateStr[i][j];j++){
if(candidateStr[i][j].left(1) == startSymbol[i]){
jugeSymbol = true;
break;
}
}
// 对输出进行格式处理
QString symbol = ""; // 开始字符
symbol += startSymbol[i]; // 开始字符转化为QString储存
QString first = symbol + "->"; // 不包含开始字符的输出
QString second = symbol + "'->"; // 包含开始字符的输出
// 对候选式进行格式处理
for(int j = 0;'\0' != candidateStr[i][j];j++){
// 有开始字符,则将结果分为两行 U->Ux|y
if(true == jugeSymbol){
if(candidateStr[i][j].left(1) != startSymbol[i]){ // 候选式的第一个字符不为开始字符
first += candidateStr[i][j]+startSymbol[i]+"'|"; // y+U+'|
} else {
QString temp = candidateStr[i][j].remove(startSymbol[i]); // 去掉开始字符
second += temp+startSymbol[i]+"'|"; // x+U+'|
}
} else { // 没有开始字符,直接输出,第二行置空 U->x|y
first += candidateStr[i][j] + "|";
second = "";
}
}
// 去掉first最后的“|”
first = first.left(first.size()-1);
// 对结果进行格式处理,有开始符号则结果有两行,第二行加上~
if(true == jugeSymbol){
first += "\n";
second += "~";
}
newGrammar += first + second + "\n";
}
五、界面效果
1、主窗口
- 主窗口
主窗口由工具栏、左输入框以及右输出框三部分组成。
工具栏中,包含:
- 清空输入窗口按钮:将CFG输入区域清空;
- 打开文件按钮:打开CFG文法文件;
- 保存文件按钮:将输出的新文法保存至txt文件中;
- 开始运行按钮:开始判断递归类型以及消除递归生成新文法;
- 退出按钮:退出程序;
- 复制按钮:将新文法复制到CFG文法输入框中,继续判断及消除递归。
左输入框处输入CFG文法,初始为提示信息;右输出框为原文法或消除左递归后的新文法输出框。
2、打开文件
- 打开指定的文法文件
3、判断递归类型
(1)不是左递归
(2)直接左递归
(3)间接左递归
4、不包含左递归
- 不包含左递归时原样输出
5、消除直接左递归
- 若为直接左递归,则消除后输出
6、消除间接左递归
- 若为间接左递归,则消除后输出
7、保存文件
- 将文法输出框中的内容把保存至指定的文件中
8、复制功能
- 将输出区域的新文法复制到输入区域
六、结
- 八个功能,分别为清除文法输入功能、文法文件的打开功能、左递归的判断功能、直接左递归的消除功能、间接左递归的消除功能、新文法的保存功能、新文法的复制功能以及可视化功能。
- 在处理文法时,选择将开始符号与候选式分开,将所有候选式作为一个二维数组,每一行为一个产生式的所有候选式,行号与开始符号对应,方便操作。在处理直接左递归时,将每一个产生式分为两行分别进行处理。在处理间接左递归时,由于间接左递归的判断与消除存在一致的地方,故将判断与消除统一处理,分别获取所需要的内容。
- 虽然经过多次修改,但是还是存在一些不足,如产生式与产生式之间需要紧凑,不能存在空行,若有空行会导致结果错误。以及没有设计能够直接将所有左递归消除的功能,需要先消除直接左递归,再消除间接左递归等不足。
- 在刚开始写的时候,对左递归把握不清楚,导致在判断间接左递归时只是单纯的判断上下两行间有无相同的开始符号,从而不能准确判断左递归类型。
- 在存储候选式时用的是QString[100][100]的二维数组,存储有限