編譯原理入門課:(五)解析ID型詞法和函數調用語法

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

上一章詞法分析的內容裏咱們介紹瞭解析數字的方法,當時還提到了對ID的解析,可是由於當時還用不到ID類型,因此就沒有作對應的解析,這一章咱們將會講解下ID類型的解析方法。前端

ID類型一般用在變量名和函數名上,變量要應用的話至少還得實現賦值表達式,因此咱們先用ID類型來嘗試實現函數調用功能。注意是調用咱們在計算器裏內置的函數,暫時尚未辦法動態定義新的函數。git

ID類型的詞法分析

有了上一章的基礎,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定義成可變的就好,具體深刻的作法這裏我也不展開了,由於那個已經超出入門課的範疇啦!

入門課總結

到這裏咱們的編譯原理入門課就告一段落了。經過實現一些簡單的表達式解析計算功能,咱們把編譯器前端的語法分析和詞法分析工做原理講了個大概。過程當中也介紹了一些設計編譯器的方法,你們掌握了以後應該也能夠在其基礎上,作出一些本身想要的功能,例如實現變量和賦值表達式等。我也只能作到領你們入個門,更深刻的知識就須要你們本身再去找資料深刻學習啦(前言裏我也列了一些資料)。

那麼但願個人入門課對你有所幫助。若是之後個人懶癌痊癒了,興許會再寫個編譯原理中級課吧~再見~😁

相關文章
相關標籤/搜索