文章目錄
一、題目要求
- 左遞歸判定:輸入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]的二維數組,存儲有限