原文地址:蘋果梨的博客html
上一章詞法分析的內容裏咱們介紹瞭解析數字的方法,當時還提到了對ID的解析,可是由於當時還用不到ID類型,因此就沒有作對應的解析,這一章咱們將會講解下ID類型的解析方法。前端
ID類型一般用在變量名和函數名上,變量要應用的話至少還得實現賦值表達式,因此咱們先用ID類型來嘗試實現函數調用功能。注意是調用咱們在計算器裏內置的函數,暫時尚未辦法動態定義新的函數。git
有了上一章的基礎,ID類型的解析應該對你們是易如反掌了,簡單到我都懶得畫狀態機圖了:github
typedef struct {
int type;
int value;
char* name;
} slm_token;
void next(slm_expr *e) {
...
if (isalpha(*e->expStr)) {
e->token.type = SLM_EXPRESSION_TOKEN_ID;
const char *start = e->expStr;
do {
(e->expStr)++;
} while (isalpha(*e->expStr) || isdigit(*e->expStr));
size_t length = e->expStr - start;
char *name = calloc(length + 1, sizeof(char));
strncpy(name, start, length);
name[length] = '\0';
e->token.name = name;
}
...
}
複製代碼
咱們在slm_token
結構體裏新增了一個name
字段用來存儲解析出的ID,這裏的ID型token以字母開頭,後面能夠跟着多位字母或數字。C語言裏的內存管理是要重點注意的,必定要在適當的時機釋放掉本身申請的內存,我一開始也漏了一處😂。數組
哈哈,又回到語法分析步驟了。回到語法分析,首先要寫的就是文法,此次只列出來要修改的文法:bash
factor -> number | func | '(' expr ')'
func -> func1 | func2
func1 -> id '(' expr ')'
func2 -> id '(' expr ',' expr ')'
複製代碼
咱們只打算支持三個函數:max(x, y)
、min(x, y)
和abs(x)
。由於這裏只有單參數和雙參數兩種狀況,因此文法就直接生硬的把全部狀況列出來了。有了給定的文法,想必寫邏輯也不是難事。由於懶得去弄一個新的變量暫存函數名字串,因此我直接把三個函數展開成三個if段來寫:函數
int func(slm_expr *e) {
if (e->token.type != SLM_EXPRESSION_TOKEN_ID || !e->token.name) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_ID);
}
int result;
if (strcmp(e->token.name, "max") == 0) {
TRY(next(e));
// '('
if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
}
TRY(next(e));
// arg1
int arg1 = TRY(expr(e));
// ','
if (e->token.type != SLM_EXPRESSION_TOKEN_COMMA) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_COMMA);
}
TRY(next(e));
// arg2
int arg2 = TRY(expr(e));
// ')'
if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
}
TRY(next(e));
// result
result = arg1 >= arg2 ? arg1 : arg2;
} else if (strcmp(e->token.name, "min") == 0) {
TRY(next(e));
// '('
if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
}
TRY(next(e));
// arg1
int arg1 = TRY(expr(e));
// ','
if (e->token.type != SLM_EXPRESSION_TOKEN_COMMA) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_COMMA);
}
TRY(next(e));
// arg2
int arg2 = TRY(expr(e));
// ')'
if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
}
TRY(next(e));
// result
result = arg1 <= arg2 ? arg1 : arg2;
} else if (strcmp(e->token.name, "abs") == 0) {
TRY(next(e));
// '('
if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
}
TRY(next(e));
// arg1
result = TRY(expr(e));
// ')'
if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
}
TRY(next(e));
// result
result = abs(result);
} else {
THROW(SLM_EXPRESSION_ERROR_UNKNOW_FUNCTION);
}
return result;
}
複製代碼
作一些測試驗證下邏輯是否正常,函數調用功能就算完成啦。到這一步的完整代碼能夠在SlimeExpressionC-chapter5.1得到。學習
不用我說,你們應該也以爲上一節的函數解析邏輯寫得太醜了,這種代碼不該該存在於咱們的庫裏!測試
首先咱們確定不能給每個函數寫一個if段,那麼咱們確定須要一個表來存儲咱們支持的全部函數,這樣就能夠在表裏查詢咱們支持的函數該怎麼處理了。而後就是要考慮函數怎麼存在內存裏呢?答案固然是用函數指針呀。咱們先作的粗糙一點,把參數數量不一樣的函數定義成不一樣的結構體成員:優化
typedef struct {
const char *name;
int argCount;
int (*func1)(int);
int (*func2)(int, int);
} slm_func;
const int FuncCount = 3;
const int ArgMaxCount = 2;
const slm_func FuncList[FuncCount] = {
{.name = "max", .argCount = 2, .func2 = &slm_max},
{.name = "min", .argCount = 2, .func2 = &slm_min},
{.name = "abs", .argCount = 1, .func1 = &slm_abs},
};
複製代碼
這樣,咱們的函數表就先搞定了。把函數指針對應的函數給實現一下:
int slm_abs(int x) {
return x < 0 ? -x : x;
}
int slm_max(int x, int y) {
return x >= y ? x : y;
}
int slm_min(int x, int y) {
return x <= y ? x : y;
}
複製代碼
接下來的重頭戲就是對func函數的改造啦,有了思路你們應該也大概能知道實現是什麼樣的了:
int func(slm_expr *e) {
if (e->token.type != SLM_EXPRESSION_TOKEN_ID || !e->token.name) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_ID);
}
for (int i = 0; i < FuncCount; i++) {
slm_func funcItem = FuncList[i];
if (strcmp(e->token.name, funcItem.name) == 0) {
TRY(next(e));
// '('
if (e->token.type != SLM_EXPRESSION_TOKEN_OPEN) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_OPEN_PARENTHESIS);
}
TRY(next(e));
// arg1
int args[ArgMaxCount];
args[0] = TRY(expr(e));
// arg2 ~ argN
for (int j = 1; j < funcItem.argCount; j++) {
// ','
if (e->token.type != SLM_EXPRESSION_TOKEN_COMMA) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_COMMA);
}
TRY(next(e));
// arg2 ~ argN
args[j] = TRY(expr(e));
}
// ')'
if (e->token.type != SLM_EXPRESSION_TOKEN_CLOSE) {
THROW(SLM_EXPRESSION_ERROR_EXPECT_CLOSE_PARENTHESIS);
}
TRY(next(e));
// result
switch (funcItem.argCount) {
case 1:
return (*funcItem.func1)(args[0]);
break;
case 2:
return (*funcItem.func2)(args[0], args[1]);
break;
}
break;
}
}
THROW(SLM_EXPRESSION_ERROR_UNKNOW_FUNCTION);
}
複製代碼
固然這代碼仍是有優化空間的,好比咱們能夠用hash表來存儲函數表,加快檢索的效率;好比咱們能夠用鏈表或者數組之類的手段傳遞參數,這樣就能夠動態的支持無限多的參數。不過這些就不深刻在這裏展開啦,目前完成的完整的代碼放在SlimeExpressionC-chapter5.2。
藉助函數表也是之後咱們實現動態定義新函數的關鍵點,到時候你們把FuncList定義成可變的就好,具體深刻的作法這裏我也不展開了,由於那個已經超出入門課的範疇啦!
到這裏咱們的編譯原理入門課就告一段落了。經過實現一些簡單的表達式解析計算功能,咱們把編譯器前端的語法分析和詞法分析工做原理講了個大概。過程當中也介紹了一些設計編譯器的方法,你們掌握了以後應該也能夠在其基礎上,作出一些本身想要的功能,例如實現變量和賦值表達式等。我也只能作到領你們入個門,更深刻的知識就須要你們本身再去找資料深刻學習啦(前言裏我也列了一些資料)。
那麼但願個人入門課對你有所幫助。若是之後個人懶癌痊癒了,興許會再寫個編譯原理中級課吧~再見~😁