語法分析
說實話,上課我能聽懂,但是,看到作業題目的我是懵逼的,到底想讓我們干什么?
在閱讀學長代碼的時候,我仿佛又明白了想讓我們干什么,就是輸出而已,可是這和上課講的符號表、語法樹有什么關系呢,為啥學長代碼里有符號表和語法樹的部分?
后來我才知道,因為是“增量開發”,我們要先寫一個大型的字符串處理器來做語法分析,然后再慢慢加上符號表等。
如何入手
先看一段代碼:
// <Decl>::= <ConstDecl> | <VarDecl>
// <ConstDecl>::= 'const' 'int' <ConstDef> {, <ConstDef>}
// <VarDecl>::= 'int' <VarDef> { ',' <VarDef> } ';'
// promise: already read a token
void Parser::Decl() {
if (type_code_ == TypeCode::CONSTTK) {
ConstDecl();
} else if (type_code_ == TypeCode::INTTK) {
VarDecl();
} else {
handle_error("expect a const or int head of <Decl>");
}
output("<Decl>");
}
首先保證,當進入Decl分析時,已經讀取了一個token,接下來,若這個token的類別碼為const時,就進入ConstDecl分析,
若為int,就進入VarDecl分析。
分析完之后,再輸出<Decl>。這就是為什么樣例的最后會輸出非終結符。
那么我們要做的事就明確了——為每一個非終結符寫一個分析函數,在函數的結尾輸出這個非終結符。
根據文法,粗粗一算,大概要寫20~30個函數。
接下來,我們具體講一講怎么寫這個遞歸下降的字符串處理器。
明確非終結符
為了寫函數,我們得明確文法中有哪些非終結符。
我的方法是,畫一棵樹,這棵樹表明了非終結符之間的依賴關系,而樹上有所有要輸出的非終結符號。
為了不遺漏地將非終結符的分析函數寫出來,我們要遍歷自己畫的這棵樹。
我畫的樹如下:

各種文法情況
首先,我們約定,進入一個非終結符函數時,已經幫這個非終結符讀取了一個token;
而從非終結符出來時,沒有再預讀。
分支處理
下面是一個典型的分支處理的例子,面對如此多的分支,進行判斷,然后進入相應的非終結符處理函數。
// Stmt-> 'if' '(' Cond ')' Stmt [ 'else' Stmt ] |
// 'while' '(' Cond ')' Stmt |
// 'return' [Exp] ';' |
// 'printf''('FormatString{,Exp}')'';'
void Parser::Stmt() {
if (type_code_ == TypeCode::IFTK) {
IfStmt();
} else if (type_code_ == TypeCode::WHILETK) {
WhileStmt();
} else if (type_code_ == TypeCode::RETURNTK) {
ReturnStmt();
} else if (type_code_ == TypeCode::PRINTFTK) {
WriteStmt();
} else {
handle_error("expect ';' end of <Stmt>");
}
output("<Stmt>");
}
當然,進入分支可能需要更多的預讀。比如變量定義、函數定義和主函數定義,在讀取到int時,還不能判斷該進入哪個分支,
這時就需要再讀取。
// 一個需要更多預讀的文法例子:
int a ( )
int a, b;
int main ( )
多次預讀取可以幫助我們確定要進入哪個分支,但是會帶來一個問題,就是進入前,讀了不只一個token。
這時,我們可以引入retract()函數,將讀取到的token放回去。
這個操作可以通過建立token讀取列表實現:retract()時,讀取序列的index回退,next_sym()時,index前進。
{} 處理
給出一些例子:
- Vn'::= { sth } xxx
- Vn'::= { Vn } xxx
- Vn'::= { Vt Vn } xxx
- Vn'::= { sth } xxx
我們要將大括號內部的句型的first集合分析出來,然后寫入while。
while (token == first_of(sth)) ) {
Vn(); // analyze Vn
next_sym(); // read a token
}
這樣我們就能保證不斷地讀取。
還有一些特殊的情況,比如xxx的first集合和Vn的first集合有交集,那么我們在while的內部就要再預讀,不滿足條件時break。
形如:Vt {Vn} Vt' 的文法還有一種處理方法——自后向前處理。用在Vn的first集合難以分析的時候。
當讀取到Vt時,再讀取一個token,若讀到Vt',說明Vn沒有,直接跳過,否則,進入while。
// already read Vt
next_sym();
while (token != Vt') {
Vn();
next_sym();
}
// now token is Vt'
左遞歸文法改寫
改寫文法簡單,但是會引起一個bug,即改寫后的文法和原來的文法,
在處理相同的句子時,會造成語法樹長得不一樣,造成輸出少了一個非終結符。
比如下面的例子,兩種文法,a && b的語法樹是不一樣的,改寫后的文法少了一個非終結符。
所以,要先擦去之前輸出的終結符,再輸出非終結符,最后補上終結符。
// <LAndExp>::= <EqExp> | <LAndExp> '&&' <EqExp>
// <LAndExp>::= <EqExp> { '&&' <EqExp> }
// note: left recurrence
// promise: already read a token
void Parser::LAndExp() {
EqExp();
next_sym();
while (type_code_ == TypeCode::AND) {
// ----------------------
retract(); // erase then token before
output("<LAndExp>"); // output the Vn
next_sym(); // output the token
// -----------------------
next_sym();
EqExp();
next_sym();
}
retract();
output("<LAndExp>");
}
檢查
- 分號的處理是否正確
- 面對{}的文法,在while出來后,是否需要回退
- 每個非終結符定義前,進入前是否預讀
bug
- 非終結符少輸出
;沒有正確處理- 左遞歸文法改寫
