任務:給定一個算術表達式的無關文法,實現一個語法分析器
分析:
根據一個上下文無關語法生成一個遞歸下降的語法分析器需要注意幾個方面(思路、步驟):
1.觀察給定語法,如果遇到左遞歸,則需要改寫語法來消除左遞歸
2.根據給定的語法,生成相應符號的First集和Fllow集
3.依照First集和Fllow集實現語法分析器的代碼
一、消除左遞歸:
原本為左遞歸的語法會使得語法分析器無限循環,無法與給定的輸入串進行匹配。我們可以使用左遞歸將其改寫為右遞歸,該方法是通用的(引入一個新的非終結符進行過渡):
這時候我們將原來的左遞歸的語法改寫成用右遞歸表示的語法:
二、進行完左遞歸后,我們要求出新生成語法中相應符號的First集和Fllow集:
該步的目的是讓語法分析器產生“預測”的能力,可以根據當前輸入中給定的字符來當前語法要進行哪個對應的語法規則轉換,從而避免因選擇了錯誤的語法分支而需要進行回溯所帶來的性能影響。
接下來講講什么是符號的First集和Fllow集。
First集是對所有的符號來說的(包括終結符和非終結符),它所表示的含義是從該符號出發,能夠推導出的所有句子(只包含終結符)中,句子開頭可能的非終結符有哪些,這些非終結符組成該符號的First集。
而Fllow集則是對所有的終結符(准確的說,是對所有FIrst集中含ε的非終結符來說)來說的,因為其First集中存在ε(即從一個非終結符出發可以不推出任何語句),所以該非終結符根據相應的語法規則推導得出的句子中,該句子的第一個符號不一定存在於它的First集合中(此時該非終結符選擇了能推導出ε所在分支的語法,記作語法X),那么要想讓他具備“預測”的能力,我們就需要知道在使用了語法X(能推導出ε的語法規則)后,其后面所跟的非終結符可能有哪些,這些非終結符就是所求的Fllow集。
First集求解:
先給出求解First集合的偽代碼,再講解一下求解First集合的步驟:
1.對於終結符的First集合,我們可以直接得到,就是該終結符本身
2.而對於非終結符的First集合,我們需要遍歷所有的推導,對於一條推導A -> B1 B2 ... Bn來說,若B1的First集合不含ε,則First(A)= First(B1),否則First(A)還包含First(B2),以此類推,直到當前所出Bi的First集合不含ε為止。這里需要注意一點只有當Bn的First集也含ε時,才將ε加入到A的First集合中
3.直到所有的First集不再變化時(不動點)
通過以上求解,我們可以得到我們所求的語法的First集合,如下表示(將終結符和非終結符區分開來表示):
Fllow集的求解:
同樣的,先給出Fllow集合的偽代碼,再對步驟進行講解:
1.對於所有的推導語句A -> B1 B2 ... Bn 來說,首先進行初始化,先將文件終結符EOF加入到所有處於推導語句左側的非終結符的Fllow集合中 ,表示所有的A語句結束后,后面都可以跟文件終結符EOF
2.對於所有的推導語句A -> B1 B2 ... Bn 來說,我們從后往前求出每個非終結符Bi的Fllow集。對於Bn來說,Fllow(Bn) = Follow(A)。
對於Bn-1來說,如果First(Bn)含ε時,Fllow(Bn-1) = First(Bn)∪Fllow(A);如果First(Bn)不含ε時,Fllow(Bn-1) = First(Bn),以此類推
3.直到所有的Fllow集不再變化時(不動點)
通過以上求解我們得到所有的非終結符的Fllow集如下表示:
三、編寫遞歸下降語法分析器
對於每一條語法的推導都有一個函數實現
有較為詳細的錯誤提示(如:第幾個字符出現錯誤,希望出現XX字符,結果出現了XX字符)
特別注意:什么時候調用其他的解析函數,什么時候報錯,什么時候利用First集合和Fllow集合進行預測,以及左右括號匹配的地方
先給出偽代碼實現:
再給出我的代碼實現:

#include <stdio.h> #include <string.h> #include <stdlib.h> char *str; size_t ind; void Expr(); void Eprime(); void Term(); void Tprime(); void Factor(); void Parse(char *s); void error(char *want, char got) { //report syntax error printf("Compling this expression:\n%s\n", str); printf ("Syntax error at position: %d\n" "\texpecting: %s\n" "\tbut got : %c\n", ind, want, got); //attempt error recovery or exit //exit(0); exit(0); return; } void Expr() { /* Expr -> Term Expr' */ Term(); Eprime(); return; } void Eprime() { /* Expr' -> + Term Expr' */ /* Expr' -> - Term Expr' */ if (str[ind] == '+' || str[ind] == '-') { //先看First集 ind++; Term(); } else if (str[ind] == '\0') { //含ε再看Fllow集 return; } else { //沒有得到期望的字符 error("\\0 or + or - or ) ", str[ind]); } return; } void Term() { /* Term -> Factor Term' */ Factor(); Tprime(); return; } void Tprime() { /* Term' -> * Factor Term' */ /* Term' -> / Factor Term' */ if (str[ind] == '*' || str[ind] == '/') { ind++; Factor(); } else if (str[ind] == '\0' || str[ind] == '+' || str[ind] == '-') { return; } else { error("\\0 or * or / or ) or + or -", str[ind]); } return; } void isNumber() { while (str[ind] >= '0' && str[ind] <= '9') { ind++; } } void isName() { while ((str[ind] >= 'a' && str[ind] <= 'z') || (str[ind] >= 'A' && str[ind] <= 'Z')) { ind++; } } void Factor() { /* Factor -> num */ /* Factor -> name */ if (str[ind] >= '0' && str[ind] <= '9') { isNumber(); return; } else if ((str[ind] >= 'a' && str[ind] <= 'z') || (str[ind] >= 'A' && str[ind] <= 'Z')) { isName(); return; } /* Factor -> ( Expr ) */ if (str[ind] == '(') { ind++; Expr(); if (str[ind] != ')') { error("number or name or )", str[ind]); } ind++; } else { error("number or name or (", str[ind]); } } void Parse(char *s) { /* Goal -> Expr */ ind = 0; str = s; Expr(); if (str[ind] != '\0') { error("\\0", str[ind]); } return; } int main( ) { char *e; /* Test */ //e = "(2)"; //Parse(e); //e = "(3+4*5))"; //Parse(e); //e = "(8-2)*3"; //Parse(e); //e = "(8-2)/3"; //Parse(e); }
如果您對文章中的內容有什么疑問,或者有錯誤的地方,可以在評論區中提出。
如果您覺得這篇文章幫到了您,或者覺得這篇文章寫的不錯,希望可以點個贊。
如果您覺得這篇文章在某些方面(包括但不限於寫法,結構,組織等方面)可以改進,希望可以在評論區中提出的自己的建議。
多謝您的參與!
文章所引用的參考資料:
《編譯器設計(第二版)》
網易雲課堂《編譯原理》(華保健)