編譯原理入門課:(四)用詞法解析處理多位數字和空白符

原文地址:蘋果梨的博客html

以前爲了快速進入主題,咱們約定了表達式裏只會出現個位數的數字。如今是時候打破這個規則,支持多位數的數字了。爲了支持這點,咱們就須要接觸一個新的步驟——詞法分析。git

詞法分析的做用

詞法分析就是把一個完整的語句拆分紅一個個詞(token),方便以後進行進一步的語法分析。github

舉個簡單的例子:今天真熱,將會被拆分紅<今天>, <真>, <熱>。固然拆分紅<今>, <天真>, <熱>也是一種可能,可是這樣的分詞方式不符合漢語的語法。正則表達式

好在計算機語言大部分是英文的,詞與詞之間通常用空白符隔開,很容易拆分。舉個代碼的例子:a = 1.1 + 2將被拆分爲<id: a>, <等號>, <浮點數: 1.1>, <加號>, <整數: 2>,固然咱們也能夠把加號和等號都算做運算符,作必定的聚合獲得<id: a>, <運算符: =>, <浮點數: 1.1>, <運算符: +>, <整數: 2>json

有人要問爲何拆分token的邏輯要作成單獨的詞法分析步驟,而不是放在語法分析裏一塊兒作?這是個好問題,還真的有點難回答。從我我的的觀點來講主要的兩點多是:函數

  1. 詞法分析不太適於用遞歸的方式來解析,性能會比較低,也不方便爲不一樣種類的token寫特有的解析邏輯
  2. 詞法分析做爲單獨的步驟,能夠更方便獨立的爲token附加各類屬性,爲後續的步驟作準備

要問得更具體的話,仍是建議各位在本身寫編譯器的過程當中自行體會一下……😂性能

上面也提到了詞法分析器主要是用來拆分token的,可是詞法分析器還要負責一些別的工做。咱們總結下詞法分析器的主要工做範圍:ui

  1. 拆分token。
  2. 過濾掉多餘的空白符,發現沒法識別的無效字符並報錯。
  3. 記錄代碼中每一個token的位置信息,方便在編譯出錯時能夠定位到具體的位置。
  4. 宏定義處理。
  5. 和符號表進行交互。例如定義函數時把函數名加入函數表,方便重複定義同名函數時進行報錯。

我準備一開始作的簡單點,先把必備的前兩條功能給實現了。spa

詞法分析的實現

定義要用到的結構體和枚舉

首先咱們得定義一個用來描述token的結構體:3d

typedef struct {
    int type;
    int value;
} slm_token;
複製代碼

關於token的類型,前文也提到了,運算符能夠作必定聚合,也能夠每種運算符算一種類型。我這裏就不作聚合了,把咱們前文出現過的token類型都定義出來:

enum {
    SLM_EXPRESSION_TOKEN_UNKNOWN = 0,
    SLM_EXPRESSION_TOKEN_DIGITS,
    SLM_EXPRESSION_TOKEN_ADD,
    ...
    SLM_EXPRESSION_TOKEN_CLOSE,
    SLM_EXPRESSION_TOKEN_END
};
複製代碼

而後擴展下咱們的slm_expr結構體,之後語法分析器就不該該直接讀expStr而應該從token裏取值啦:

typedef struct {
    const char *expStr;
    slm_token token;
    int errType;
} slm_expr;
複製代碼

詞法分析核心實現及狀態機

詞法分析的核心函數通常叫作next或者scan,咱們這裏就叫它next吧。它主要實現的功能是讀取下一個有效的token,存到slm_expr結構體的token成員裏以供語法分析器使用。

C語言的詞法分析十分簡單,由於根據token的首字符就能區分出token的類型:若是首字符是數字那必定是個數值token;若是首字符是字母或下劃線那必定是個id類的token,至於這個id是關鍵字仍是函數名、變量名那就另說了。怎麼樣?是否是忽然明白了大部分計算機語言裏變量名不能以數字開頭的緣由?

這一章裏面咱們暫時還用不到id類的token,因此主要講一下數值token的處理:

void next(slm_expr *e) {
    ...
    if (isdigit(*e->expStr)) {
        e->token.type = SLM_EXPRESSION_TOKEN_DIGITS;
        e->token.value = *e->expStr - '0';
        (e->expStr)++;
        while (isdigit(*e->expStr)) {
            e->token.value = e->token.value * 10 + (*e->expStr - '0');
            (e->expStr)++;
        }
    }
    ...
}
複製代碼

可見若是發現一個token是以數字開頭的,那麼咱們能夠循環讀取後面連續的數字,直接把整個token完整的數字值讀取出來,供語法分析器在後面的分析中使用。

詞法分析的過程通常能夠用狀態機來描述,上面的解析過程對應的狀態機能夠用這麼個圖來表示:

09-A

能夠看出來這種圖和流程圖類似,更適合用條件分支及循環語句來實現它的邏輯。而後咱們能夠大體的補全一下整個詞法分析器的狀態機圖:

09-B

接下來照着狀態機圖來實現代碼邏輯就行了,在這裏不貼完整代碼了。注意若是出現了用狀態機沒法描述的token,那麼這必定是個非法的token。

用狀態機圖能夠直觀的表示詞法分析的流程,之後擴展數值類型支持浮點數之類的,均可以從畫狀態機圖開始。好比大部分計算機語言支持的數字類型,能夠用下面的狀態機圖來表示(圖片來自Online JSON Viewer):

09-C

除了狀態機,另外一個超級適於描述詞法分析器的就是正則表達式,有興趣的同窗能夠自行去了解下。著名的詞法分析器Lex就是用正則表達式描述詞法規則的。

給語法分析器接入詞法分析器

以最典型的number函數爲例:

int number(slm_expr *e) {
    int hasMinus = 0;
    if (e->token.type == SLM_EXPRESSION_TOKEN_SUB_OR_MINUS) {
        TRY(next(e));
        hasMinus = 1;
    }
    if (e->token.type != SLM_EXPRESSION_TOKEN_DIGITS) {
        THROW(SLM_EXPRESSION_ERROR_EXPECT_DIGIT);
    }
    int result = e->token.value;
    TRY(next(e));
    if (hasMinus) {
        result *= -1;
    }
    return result;
}
複製代碼

咱們把以前從e->expStr直接取值的代碼都換成讀取e->token。還要把(e->expStr)++的地方都替換爲TRY(next(e)),加上TRY是由於next裏面也會報非法token的錯誤。固然不能忘記的是,在main函數裏必須預先調用一次next,否則首次進入語法解析器的時候e->token會是空的。

把全部語法分析步驟裏的代碼替換完以後,咱們就能夠獲得一個能剔除空格、識別非法字符和多位數字的解析器啦。完整的代碼我就不在這裏全貼出來了,存放在SlimeExpressionC,歡迎你們自取。

相關文章
相關標籤/搜索