語法分析


語法分析

說實話,上課我能聽懂,但是,看到作業題目的我是懵逼的,到底想讓我們干什么?

在閱讀學長代碼的時候,我仿佛又明白了想讓我們干什么,就是輸出而已,可是這和上課講的符號表、語法樹有什么關系呢,為啥學長代碼里有符號表和語法樹的部分?

后來我才知道,因為是“增量開發”,我們要先寫一個大型的字符串處理器來做語法分析,然后再慢慢加上符號表等。

如何入手

先看一段代碼:

// <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>");
}

檢查

  1. 分號的處理是否正確
  2. 面對{}的文法,在while出來后,是否需要回退
  3. 每個非終結符定義前,進入前是否預讀

bug

  1. 非終結符少輸出
  2. ;沒有正確處理
  3. 左遞歸文法改寫


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM