從0開始自制解釋器——重構代碼

在上一篇文章中,完成了對括號的支持,這樣整個程序就可以解析普通的算術表達式了。但是在解析兩個括號的過程中發現有大量的地方需要進行索引的回退操作,索引的操作應該保證能得到爭取的token,這個步驟應該放在詞法分析的階段,如果在語法分析階段還要考慮下層詞法分析的過程,就顯得有些復雜了。而且隨著后續支持的符號越來越多,可能又得在大量的地方進行這種索引變更的操作,代碼將難以理解和維護。因此這里先停下來進行一次代碼的重構。

基本架構

這里的代碼我按照教程里面的結構進行組織。將按照程序的邏輯分為3層,最底層負責操作字符串的索引保證下次獲取token的時候索引能在正確的位置。第二層是詞法分析部分,負責給字符串的每個部分都打上對應的token。第三個部分是語法分析的部分,它負責解析之前設計的BNF范式,并計算對應的結果。

詳細的代碼

上面給出模塊劃分的概要可能沒怎么說清楚,下面將通過代碼來進行詳細的說明。

Token 模塊

為了支持這個設計,首先變更一下全局變量的定義,現在定義的全局變量如下所示

extern Token g_currentToken; //當前token
extern int g_nPosition; //當前字符索引的位置
extern char g_currentChar; //當前字符串

之前通過 get_next_char() 來返回當前指向的token并變更索引的時候發現我們在任何時候想獲取當前指向的字符時永遠要變更索引,這樣就不得不考慮在某些時候要進行索引的回退。比如在解析整數退出的時候,此時當前字符已經指向下一個字符了,但是我們在接下來解析其他符號的時候調用 get_next_char() 導致索引多增加了一個。這種情況經常出現,因此這里使用全局變量保存當前字符,只在需要進行索引增加的時候進行增加。另外我們不希望上層來直接操作這個索引,因此在最底層的Token模塊提供一個名為 advance() 的函數用于將索引加一,并獲取之后的字符。它的定義如下

void advance()
{
    g_nPosition++;
    // 如果到達字符串尾部,索引不再增加
    if (g_nPosition >= strlen(g_pszUserBuf))
    {
        g_currentChar = '\0';
    }
    else
    {
        g_currentChar = g_pszUserBuf[g_nPosition];
    }
}

這樣在對應需要用到當前字符的位置就不再使用 get_next_char() , 而是改用全局變量 g_currentChar。例如現在的 skip_whitespace 函數現在的定義如下

void skip_whitespace()
{
    while (is_space(g_currentChar))
    {
        advance();
    }
}

這樣我們在獲取下一個token的時候只在必要的時候進行索引的遞增。

lex 模塊

由于打標簽的工作交個底層的Token模塊了,該模塊主要用來實現詞法分析的功能,也就是給各個部分打上標簽,根據之前Token部分提供的接口,需要對 get_next_token 函數進行修改。

bool get_next_token()
{
    dyncstring_reset(&g_currentToken.value);
    while (g_currentChar != '\0')
    {
        if (is_digit(g_currentChar))
        {
            g_currentToken.type = CINT;
            parser_number(&g_currentToken.value);
            return true;
        }
        else if (is_space(g_currentChar))
        {
            skip_whitespace();
        }
        else
        {
            switch (g_currentChar)
            {
                case '+':
                    g_currentToken.type = PLUS;
                    dyncstring_catch(&g_currentToken.value, '+');
                    advance();
                    break;
                case '-':
                    g_currentToken.type = MINUS;
                    dyncstring_catch(&g_currentToken.value, '-');
                    advance();
                    break;
                case '*':
                    g_currentToken.type = DIV;
                    dyncstring_catch(&g_currentToken.value, '*');
                    advance();
                    break;
                case '/':
                    g_currentToken.type = MUL;
                    dyncstring_catch(&g_currentToken.value, '/');
                    advance();
                    break;
                case '(':
                    g_currentToken.type = LPAREN;
                    dyncstring_catch(&g_currentToken.value, '(');
                    advance();
                    break;
                case ')':
                    g_currentToken.type = RPAREN;
                    dyncstring_catch(&g_currentToken.value, ')');
                    advance();
                    break;
                case '\0':
                    g_currentToken.type = END_OF_FILE;
                    break;
                default:
                    return false;
            }

            return true;
        }
    }

    return true;
}

在這個函數中,將不再通過輸出參數來返回當前的token,而是直接修改全局變量。同時也不再使用get_next_char 函數來獲取當前指向的字符,而是直接使用全局變量。并且在適當的時機調用advance 來實現遞增。

另外在上層我們直接使用 g_currentToken 拿到當前的token,而在適當的時機調用新增的eat() 函數來實現更新token的操作。

bool eat(LPTOKEN pToken, ETokenType eType)
{
    if (pToken->type == eType)
    {
        get_next_token();
        return true;
    }

    return false;
}

該函數接受兩個參數,第一個是當前token的值,第二個是我們期望當前token是何種類型。如果當前token的類型與期望的不符則報錯,否則更新token。

interpreter 模塊

該模塊主要負責解析根據前面的BNF范式來完成計算并解析內容。這個模塊提供三個函數get_factorget_termexpr。這三個函數的功能沒有變化,只是在實現上依靠lex 模塊提供的功能。主要思路是直接使用 g_currentToken 這個全局變量來獲得當前的token,使用 eat() 來更新并獲得下一個token的值。這里我們以get_factor() 函數為例

int get_factor(bool* pRet)
{
    int value = 0;
    if (g_currentToken.type == CINT)
    {
        value = atoi(g_currentToken.value.pszBuf);
        *pRet = eat(&g_currentToken, CINT);
    }
    else
    {
        if (g_currentToken.type == LPAREN)
        {
            bool bValid = true;
            bValid = eat(&g_currentToken, LPAREN);
            value = expr(&bValid);
            bValid = eat(&g_currentToken, RPAREN);
            *pRet = bValid;
        }
    }

    return value;
}

與前面分析的相同,該函數主要負責獲取整數和計算括號中子表達式的值。在解析完整數和括號中的子表達式之后,需要調用eat分別跳過對應的值。只是在識別到括號之后需要跳過左右兩個括號。

這樣就完成了對應的分層,每層只負責自己該做的事。不用在上層考慮修改索引的問題,結構也更加清晰,未來在添加功能的時候也更加方便。剩下幾個函數就不再貼出代碼了,感興趣的小伙伴可以去對應的GitHub倉庫上查閱相關代碼。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容