在上一篇文章中,完成了對括號的支持,這樣整個程序就可以解析普通的算術表達式了。但是在解析兩個括號的過程中發現有大量的地方需要進行索引的回退操作,索引的操作應該保證能得到爭取的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_factor
、get_term
、expr
。這三個函數的功能沒有變化,只是在實現上依靠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倉庫上查閱相關代碼。