編譯器實現之旅——第三章 實現詞法分析器前的準備

在這一章的旅程中,咱們將要爲整個編譯器的「前端中的前端」:詞法分析器的實現作好充足的準備。前端

1. 詞法分析器概觀

縱觀編譯器的輸入:源代碼,咱們不難發現,源代碼說白了也就是一個很長很長的字符串。而說到字符串,咱們不難想到字符串的分割函數。這類分割函數以空格,或任意的什麼字符或字符串做爲分隔符,將一個字符串分割成多個小字符串片斷。這不就是詞法分析器麼?你可能會想。可是,咱們將遇到這樣的問題:算法

"1 + 1" -> ("1", "+", "1")
    "1+1"   -> ?

確實,使用普通的字符串分割函數能夠很輕易的將上面第一個字符串進行分割,但咱們發現,不管怎麼設置分隔符,咱們都沒法將第二個字符串也分割成一樣的結果了。也就是說,普通的字符串分割函數及其算法是不能勝任詞法分析器的工做的,咱們必須另想辦法。函數

要想分割一個字符串,其思路無非就是尋找一個分割點,而後將當前起點到分割點的這段字符串分割出去,再將當前起點設置於分割點以後,並繼續尋找下一個分割點,不斷重複這個過程,直至到達字符串的結尾。那麼,爲何字符串分割函數不能勝任詞法分析器的工做呢?略加思索不難發現緣由:字符串分割函數的「尋找下一個分割點」的邏輯過於簡單了,只是一個相等性判斷。而咱們所須要的邏輯更復雜,好比:看到一個空格,就分割;再好比:看到一個不是數字的字符,就分割;等等。因此,只要咱們擴充字符串分割函數的「尋找下一個分割點」的邏輯,咱們就能實現出詞法分析器了。code

2. 詞法分析器的狀態

咱們首先須要作什麼呢?咱們須要爲詞法分析器定義許多不一樣的狀態,處於不一樣狀態的詞法分析器執行不一樣的行爲。顯然,詞法分析器須要一個開始狀態,一個完成狀態,其可能還須要一個或多箇中間狀態。詞法分析器從開始狀態開始,不斷讀取源代碼中的每一個字符,最終結束於完成狀態,當詞法分析器處於完成狀態時,其就分割出了一個記號。詞法分析器不斷執行這樣的「開始, ..., 完成」過程,直至到達字符串的結尾。token

爲了獲知詞法分析器到底須要哪些狀態,咱們須要看一看CMM語言對於記號的定義。請注意,這裏的記號是廣義的,其不只表明一個英文單詞,還表明一個符號,一串數字等,即,一個記號就是詞法分析器須要分割出來的一段字符串。CMM語言對於記號的定義以下所示:字符串

  1. 一串連續的,由大寫或小寫字母構成的字符串
  2. 一串連續的,由數字構成的字符串
  3. 這些符號:+ - * / < <= > >= == != = ; , ( ) [ ] { }
  4. /* ... */ 構成註釋
  5. 關鍵詞:void int if else while return

這裏須要說明的是:所謂關鍵詞,僅僅是上述第1條的一種特例。即:當咱們分割出一個單詞時,咱們須要額外斷定一下這個單詞是否是關鍵詞,若是是,則咱們須要將這個單詞的類別從「單詞」變爲「關鍵詞XX」。例如:當咱們分割出字符串「abc」時,咱們將其歸類爲「單詞」;而當咱們分割出字符串「if」時,咱們就須要將其歸類爲「關鍵詞if」。編譯器

有了CMM語言對於記號的定義,咱們就能夠着手考慮詞法分析器到底須要哪些狀態了。咱們不妨以上述第一條規則爲例進行思考,即:爲了分割出一個單詞,詞法分析器須要哪些狀態?string

首先,詞法分析器從「開始」狀態開始,若是此時詞法分析器讀入了一個大寫或小寫字母,則咱們知道:接下來讀取到的將是一個單詞了;但同時,僅憑讀取到的這個字符,咱們永遠不可能知道當前的這個單詞是否已經讀取結束;咱們只有看到一個不是大寫或小寫字母的字符時,才能肯定剛剛的這個單詞已經讀取結束了,咱們應令詞法分析器進入「完成」狀態。爲了處理這種狀況,咱們引入中間狀態「正在讀取單詞」。當詞法分析器讀入一個大寫或小寫字母時,其應當即由「開始」狀態轉入「正在讀取單詞」狀態;詞法分析器應保持這個狀態,並不斷讀入新的字符,直至當前讀入的字符不是大寫或小寫字母,此時,詞法分析器應當即由「正在讀取單詞」狀態轉入「完成」狀態,完成這次分割。it

那麼,如何利用上述思路,使詞法分析器跳過註釋呢?請看:編譯

首先,詞法分析器仍是從「開始」狀態開始,當其讀入一個「/」時,咱們此時並不知道這個「/」是一個除號,仍是註釋的開始,故咱們先令詞法分析器進入「正在讀取除號」這個中間狀態。在此狀態中,若是詞法分析器讀入的下一個字符是一個「」,則此時咱們就能夠肯定詞法分析器如今進入了註釋中,咱們就再令詞法分析器轉入「正在讀取註釋」狀態;反之,若是詞法分析器讀入的下一個字符不是一個「」,咱們也能夠肯定詞法分析器此次讀取到的真的是一個除號,此時,咱們固然是令詞法分析器進入「完成」狀態。

當詞法分析器處於「正在讀取註釋」狀態中時,咱們須要關注兩件事:

  1. 詞法分析器應丟掉任何讀取到的字符
  2. 詞法分析器應努力的逃離註釋

怎麼逃離註釋呢?顯然,若是要逃離註釋,咱們就須要同時知足這兩個條件:

  1. 遇到一個「*」
  2. 緊接着,再遇到一個「/」

因此,當詞法分析器被困在註釋中時,其一邊一視同仁的丟掉一切讀取到的字符,一邊也留心着讀取到的字符是否是「」,若是是,詞法分析器就看到了但願。此時,詞法分析器應轉入「正在逃離註釋」狀態,在這個狀態下,若是詞法分析器又讀取到了「/」,那麼恭喜,此時詞法分析器就成功的逃離了註釋,又回到了久違的「開始」狀態;若是不是「/」,但願也沒有徹底破滅,此時,若是詞法分析器讀取到的仍是「」,那麼其就還應該停留在「正在逃離註釋」狀態;而若是讀取到的既不是「/」也不是「*」,那麼很遺憾,逃離就完全失敗了,詞法分析器又將回退到「正在讀取註釋」狀態。

利用上述思路觸類旁通,咱們便可獲得詞法分析器所須要的全部狀態了。請看:

  1. 顯然,咱們須要「開始」和「完成」狀態
  2. 爲了讀取單詞和數字,咱們須要「正在讀取單詞」和「正在讀取數字」狀態
  3. 爲了處理註釋相關問題,咱們須要「正在讀取除號」、「正在讀取註釋」和「正在逃離註釋」狀態
  4. 爲了明確詞法分析器讀取到的究竟是「<」仍是「<=」、是「>」仍是「>=」、是「=」仍是「==」,咱們須要「正在讀取小於號」、「正在讀取大於號」和「正在讀取等號」狀態
  5. 爲了使詞法分析器正確的讀取到「!=」(而不是「!」 + 別的錯誤符號),咱們須要「正在讀取不等號」狀態

至此,咱們就獲得了詞法分析器所須要的全部狀態。代碼以下所示:

enum class LEXER_STAGE
{
    // Start
    START,

    // abc...
    //  ^^^^^
    IN_ID,

    // 123...
    //  ^^^^^
    IN_NUMBER,

    // /?
    //  ^
    IN_DIVIDE,

    // /* ...
    //    ^^^
    IN_COMMENT,

    // ... */
    //      ^
    END_COMMENT,

    // <?
    //  ^
    IN_LESS,

    // >?
    //  ^
    IN_GREATER,

    // =?
    //  ^
    IN_ASSIGN,

    // !?
    //  ^
    IN_NOT,

    // Done
    DONE,
};

3. 記號的類別

當詞法分析器讀取到一個記號後,咱們就須要將其進行歸類。有了詞法分析器的各類狀態的輔助,這樣的歸類將變的十分容易。例如,當咱們從「正在讀取數字」狀態轉移至「完成」狀態時,咱們固然知道當前的這個記號的類別是「數字」;而當咱們讀取到一個「(」時,咱們固然也知道這個記號的類別是「左圓括號」;以此類推。咱們能夠從上文中給出的記號的定義中,獲得全部記號的類別。代碼以下所示:

enum class TOKEN_TYPE
{
    // Word
    ID,                    // ID
    NUMBER,                // Number

    // Keyword
    VOID,                  // void
    INT,                   // int
    IF,                    // if
    ELSE,                  // else
    WHILE,                 // while
    RETURN,                // return

    // Operator
    PLUS,                  // +
    MINUS,                 // -
    MULTIPLY,              // *
    DIVIDE,                // /
    LESS,                  // <
    LESS_EQUAL,            // <=
    GREATER,               // >
    GREATER_EQUAL,         // >=
    EQUAL,                 // ==
    NOT_EQUAL,             // !=
    ASSIGN,                // =
    SEMICOLON,             // ;
    COMMA,                 // ,
    LEFT_ROUND_BRACKET,    // (
    RIGHT_ROUND_BRACKET,   // )
    LEFT_SQUARE_BRACKET,   // [
    RIGHT_SQUARE_BRACKET,  // ]
    LEFT_CURLY_BRACKET,    // {
    RIGHT_CURLY_BRACKET,   // }

    // EOF
    END_OF_FILE,           // EOF

    // AST
    DECL_LIST,             // AST: DeclList
    VAR_DECL,              // AST: VarDecl
    FUNC_DECL,             // AST: FuncDecl
    PARAM_LIST,            // AST: ParamList
    PARAM,                 // AST: Param
    COMPOUND_STMT,         // AST: CompoundStmt
    LOCAL_DECL,            // AST: LocalDecl
    STMT_LIST,             // AST: StmtList
    IF_STMT,               // AST: IfStmt
    WHILE_STMT,            // AST: WhileStmt
    RETURN_STMT,           // AST: ReturnStmt
    EXPR,                  // AST: Expr
    VAR,                   // AST: Var
    SIMPLE_EXPR,           // AST: SimpleExpr
    ADD_EXPR,              // AST: AddExpr
    TERM,                  // AST: Term
    CALL,                  // AST: Call
    ARG_LIST,              // AST: ArgList
};

須要說明的是,上述代碼的最後一部分是AST節點類別,與詞法分析器無關。咱們將在後續的旅程中講述這部分類別的做用。

4. 其餘準備工做

在實現詞法分析器以前,咱們還有一些比較簡單的準備工做須要作,列舉以下:

  1. 咱們須要定義一個用於保存記號的結構體:
struct Token
{
    // Attribute
    TOKEN_TYPE tokenType;
    string tokenStr;
    int lineNo;
};

在這個結構體中,咱們保存了記號的類別、記號字符串,以及這個記號在源代碼中所處的行數。

  1. 咱們須要定義一個哈希表,以完成普通單詞到關鍵詞的識別與轉換:
const unordered_map<string, TOKEN_TYPE> KEYWORD_MAP
{
    {"void",   TOKEN_TYPE::VOID},
    {"int",    TOKEN_TYPE::INT},
    {"if",     TOKEN_TYPE::IF},
    {"else",   TOKEN_TYPE::ELSE},
    {"while",  TOKEN_TYPE::WHILE},
    {"return", TOKEN_TYPE::RETURN},
};

經過鍵的存在性檢測,咱們就能夠斷定一個單詞是不是一個關鍵詞了;若是是,咱們也能夠獲得這個關鍵詞所對應的記號的類別。

  1. 咱們須要定義一個報錯函數,用於在詞法分析器發現語法錯誤時報錯並退出:
void InvalidChar(char invalidChar, int lineNo)
{
    printf("Invalid char: %c in line: %d\n", invalidChar, lineNo);

    exit(1);
}

至此,咱們就完成了全部準備工做,能夠開始實現詞法分析器了。請看下一章:《實現詞法分析器》。

相關文章
相關標籤/搜索