用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(3)- 詞法分析

用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(3)- 詞法分析

項目github地址及源碼:
https://github.com/yunwei37/tryC

用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(1)- 目標和前言

用c語言手搓一個600行的類c語言解釋器: 給編程初學者的解釋器教程(2)- 簡介和設計

這一篇講講在tryC中詞法分析器是怎樣構建的

詞法分析器是什麼玩意

回想一下上一篇我們說的詞法分析階段,編譯器做了這樣一件事:

對源程序進行閱讀,並將字符序列,也就是源代碼中一個個符號收集到稱作記號(token)的單元中

幫編譯器執行詞法分析階段的模塊,就叫詞法分析器啦。詞法分析器能夠對源碼字符串做預處理,以減少語法分析器的複雜程度。

詞法分析器以源碼字符串爲輸入,輸出爲標記流(token stream),即一連串的標記,比如對於源代碼中間:

    num = 123.4;

這樣一個賦值語句中,變量num算是一個token,“=”符號算是一個token,“123.4”算是一個token;每個token有自己的類別和屬性,比如“123.4”的類別是數字,屬性(值)是123.4;每個token可以用這一對兒表示:{token, token value},就像“123.4”可以表示爲{Num, 123.4}

詞法分析器輸入上面那句話,就得到這樣一個標記流:

{Sym, num}, {'=', assign}, {Num, 123.4}

詞法分析器的具體實現

由於詞法分析器對於各個語言基本都是大同小異,在其他地方也有很多用途,並且手工構造的話實際上是一個很枯燥又容易出錯的活計,因此其實已經有了不少現成的實現,比如 lex/flex 。

通常詞法分析器的實現會涉及到正則表達式、狀態機的一些相關知識,或者通過正則表達式用上面那些工具來生成。但對於我們這樣一個簡單的解釋器來說,手工構造詞法分析器,並且完全不涉及到正則表達式的知識,理解起來也並不是很困難啦。

先來看看token是怎樣寫的

token的數據結構如下:


int token;                      // current token type
union tokenValue {              // token value
    symbol* ptr;               
    double val;                 
} token_val;
  • 用一個整型變量 token 來表示當前的 token 是什麼類型的;
  • 用一個聯合體來表示附加的token屬性,ptr可以附加指針類型的值,val可以附加數值。

token 的類型採用枚舉表示定義:

/* tokens and classes (operators last and in precedence order) */
enum {
    Num = 128, Char, Str, Array, Func,
    Else, If, Return, While, Print, Puts, Read,
    Assign, OR, AND, Equal, Sym, FuncSym, ArraySym, Void,
    Nequal, LessEqual, GreatEqual, Inc, Dec
};

比如我們會將“==”標記爲Equal,將Num標記爲Sym等等。從這裏也可以看出,一個標記(token)可能包含多個字符;而詞法分析器能減小語法分析複雜度的原因,正是因爲它相當於通過一定的編碼(採用標記來表示一定的字符串)來壓縮和規範化了源碼。

另外,一些單個字符我們直接作爲token返回,比如:

'}' '{' '(' ')' ';' '[' ']' .....

詞法分析器真正幹活的函數們

首先需要說明一下,源碼字符串爲輸入,輸出爲標記流(token stream),這裏的標記流並不是一次性將所有的源代碼翻譯成長長的一串標記串,而是需要一個標記的時候再轉換一個標記,原因如下:

  1. 字符串轉換成標記流有時是有狀態的,即與代碼的上下文是有關係的。
  2. 保存所有的標記流沒有意義且浪費空間。

所以通常的實現是提供一個函數,每次調用該函數則返回下一個標記。這裏說的函數就是 next() 。

這是next()的基本框架:其中“AAA”"BBB"是token類型;

void next() {
    while (token = *src) {
        ++src;
        if(token == AAA ){
            .....
        }else if(token == BBB ){
            .....
        }
    }
}

用while循環的原因有以下幾個:

  • 處理錯誤:
    如果碰到了一個我們不認識的字符,可以指出錯誤發生的位置,然後用while循環跳過當前錯誤,獲取下一個token並繼續編譯;

  • 跳過空白字符;
    在我們實現的tryC語言中,空格是用來作爲分隔用的,並不作爲語法的一部分。因此在實現中我們將它作爲“不識別”的字符進行跳過。

現在來看看AAA、BBB具體是怎樣判斷的:

換行符和空白符

...
if (token == '\n') {
    old_src = src;              // 記錄當前行,並跳過;
}
else if (token == ' ' || token == '\t') {        }
...

註釋

...
else if (token == '#') {            // skip comments
    while (*src != 0 && *src != '\n') {
                src++;
    }
}
...

單個字符

...
else if ( token == '*' || token == '/'  || token == ';' ||  token == ',' ||
token == '(' || token == ')' || token == '{' || token == '}' ||  token == '[' || token == ']') {
    return;
}

...

數字

token 爲Num;
token_val.val爲值;

...
else if (token >= '0' && token <= '9') {        // process numbers
    token_val.val = (double)token - '0';
    while (*src >= '0' && *src <= '9') {
        token_val.val = token_val.val * 10.0 + *src++ - '0';
    }
    if (*src == '.') {
        src++;
        int countDig = 1;
        while (*src >= '0' && *src <= '9') {
            token_val.val = token_val.val + ((double)(*src++) - '0')/(10.0 * countDig++);
        }
    }
    token = Num;
    return;
}

...

字符串

token 爲Str;
token_val.ptr保存字符串指針;

...
        else if (token == '"' ) {               // parse string
            last_pos = src;
            char tval;
            int numCount = 0;
            while (*src != 0 && *src != token) {
                src++;
                numCount++;          
            }
            if (*src) {
                *src = 0;
                token_val.ptr = malloc(sizeof(char) * numCount + 8);
                strcpy(token_val.ptr, last_pos);
                *src = token;
                src++;
            }
            token = Str;
            return;
        }

...

字符

token 爲Char;
token_val.val爲值;

...
        else if (token == '\'') {               // parse char
            token_val.val = *src++;
            token = Char;
            src++;
            return;
        }

...

變量:這是最複雜的一部分

對變量的處理需要以下幾個步驟:

  1. 獲取完整的變量名:
  2. 在符號表中查找變量:
  3. 如果在符號表中找到了變量,根據變量不同的類型,返回不同的token值;
  4. 如果沒有找到,在符號表中間插入新的變量

關於符號表具體的內容,會獨立出一篇文章來解釋。

...
        else if ((token >= 'a' && token <= 'z') || (token >= 'A' && token <= 'Z') || (token == '_')) {
            last_pos = src - 1;             // process symbols
            char nameBuffer[MAXNAMESIZE];
            nameBuffer[0] = token;
            while ((*src >= 'a' && *src <= 'z') || (*src >= 'A' && *src <= 'Z') || (*src >= '0' && *src <= '9') || (*src == '_')) {
                nameBuffer[src - last_pos] = *src;
                src++;
            }
            nameBuffer[src - last_pos] = 0;                 // get symbol name
            int i;
            for (i = symPointer-1; i >= 0; --i) {           // search symbol in symbol table 
                if (strcmp(nameBuffer, symtab[i].name) == 0) {      // if find symbol: return the token according to symbol type
                    if (symtab[i].type == Num || symtab[i].type == Char) {
                        token_val.ptr = &symtab[i];
                        token = Sym;
                    }
                    else if (symtab[i].type == FuncSym) {
                        token_val.ptr = &symtab[i];
                        token = symtab[i].type;
                    }
                    else if (symtab[i].type == ArraySym) {
                        token_val.ptr = &symtab[i];
                        token = symtab[i].type;
                    }
                    else {
                        if (symtab[i].type == Void) {
                            token = Sym;
                            token_val.ptr = &symtab[i];
                        }
                        else token = symtab[i].type;
                    }
                    return;
                }
            }
            strcpy(symtab[symPointer].name, nameBuffer);        // if symbol not found, create a new one 
            symtab[symPointer].levelNum = currentlevel;
            symtab[symPointer].type = Void;
            token_val.ptr = &symtab[symPointer];
            symPointer++;
            token = Sym;
            return;
        }
...

其他的一些符號,可能需要再多讀取一個字符才能確定具體token

...
        else if (token == '=') {            // parse '==' and '='
            if (*src == '=') {
                src++;
                token = Equal;
            }
            return;
        }
        else if (token == '+') {            // parse '+' and '++'
            if (*src == '+') {
                src++;
                token = Inc;
            }
            return;
        }
        else if (token == '-') {            // parse '-' and '--'
            if (*src == '-') {
                src++;
                token = Dec;
            }
            return;
        }
        else if (token == '!') {               // parse '!='
            if (*src == '=') {
                src++;
                token = Nequal;
            }
            return;
        }
        else if (token == '<') {               // parse '<=',  or '<'
            if (*src == '=') {
                src++;
                token = LessEqual;
            }
            return;
        }
        else if (token == '>') {                // parse '>=',  or '>'
            if (*src == '=') {
                src++;
                token = GreatEqual;
            }
            return;
        }
        else if (token == '|') {                // parse  '||'
            if (*src == '|') {
                src++;
                token = OR;
            }
            return;
        }
        else if (token == '&') {                // parse  '&&'
            if (*src == '&') {
                src++;
                token = AND;
            }
            return;
        }

...

錯誤處理

...
        else {
            printf("unexpected token: %d\n", token);
        }

...

可對照源碼查看
https://github.com/yunwei37/tryC

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章