寫一個不需要腦子的解釋器

-1.前言

什麼,你要寫AC自動機?什麼,你要學編譯原理?老哥你在逗我嗎?我寫解釋器就是爲了休閒娛樂,自己亂搞,搞得那麼專業幹什麼?

曾經寫過一個小巧的“解釋器”,解釋的是一門我自己YY出來的垃圾語言,雖說功能奇弱,不過從語法上講也算得上要if有if,要while有while,足夠用來計算一些簡單的小學數學題,比如,判斷個閏年、打印個一萬以內的質數表之類的(歐拉篩?想得美,我沒有數組…)。某C#資深大神看了我上一個版本解釋器的C++源代碼之後,深感代碼醜陋,邏輯性極差,不堪卒讀(請同學們注意,成語“不堪卒讀”和“不忍卒讀”在含義上有着微妙地差異),缺少解釋器所應具備的大多數組成成分。爲此,我打算重新寫一個…(我還真是個“倔強(youqu)”的人啊。)

0.暴力載滿,愉悅身心。

好了,作爲一個熱愛信競(baoli)的孩子,我們一同踏上了一條靜態查錯的不歸路。警告:下文內容純屬胡扯,如果有人因爲信了我的話,導致了不良後果(比如掛科之類的),我絕不負責。解釋的語言仍是我自己YY出來的,但與之前的相比有所不同。

I.代碼中使用的命名規則

  1. 全部標識符 一律採用駝峯式命名法。
  2. 變量、命名空間(namespace)名首字母一律小寫。
  3. 自定義的常量、類名、結構體名、函數名、首字母一律大寫(函數名main除外)。

II.註釋規則

  1. 函數定義所在行應有解釋函數功能的註釋。
  2. return所在行應有解釋return內容的註釋。

1.詞法分析

把混亂的文件(用暴力的方法),拆成單詞的組合,從而提升文件的有序性。我打算浪費一點硬盤空間和處理時間,每一步處理時,都生成出一個處理中間文件,用以分析程序錯誤。下一步的處理,將會利用到上一步生成的中間文件。

在實際操作之前,我們需要做一些必要的定義。

I.爲字符分類

由於我打算模糊化對漢字等特殊符號的處理,所以,在這裏我們姑且認爲一個字節(Byte)中所儲存的信息,就代表一個字符。爲了描述方便我們將字符分爲以下幾類:

編號 分類 ASCII碼範圍 集合名稱
1 特殊符 [128,2][-128,-2] SpecialChar
2 文件尾 {1}\{-1\} EndOfFile (EOF)
3 控制符 [0,8]{11,12,127}[14,31][0,8]\cup\{11, 12, 127\}\cup[14,31] Controller
4 空白符 {32,9,10,13}\{32, 9, 10, 13\} SpaceChar
5 算符 [33,47][58,64][91,94]{96}[123,126][33,47]\cup[58,64]\cup[91,94]\cup\{96\}\cup[123,126] Operator
6 數字 [48,57][48,57] NumeralChar
7 大寫字母 [65,90][65,90] CapitalChar
8 小寫字母 [97,122][97,122] LowerChar
9 下劃線 {95}\{95\} UnderLine

以上的九個集合,覆蓋了一個字節能表示的全部符號,寫一段簡單而無聊的代碼實現字符分類。

const int SpecialChar = 1, /// 定義了一些與字符分類有關的常量 
          EndOfFile   = 2,
          Controller  = 3,
          SpaceChar   = 4,
          Operator    = 5,
          NumeralChar = 6,
          CapitalChar = 7,
          LowerChar   = 8,
          UnderLine   = 9;

#define Rin(A, B, C) (((A)<=(B))&&((B)<=(C))) /// 判斷 B是否在閉區間 [A, C]內  

int GetCharType(char c) { /// 實現字符分類 
    if(Rin(-128, c, -2)) return SpecialChar; /// 特殊字符 
    else
    if(c == -1) return EndOfFile; /// 文件尾 
    else
    if(Rin(0, c, 8) || Rin(14, c, 31) || Rin(11, c, 12) || c == 127) return Controller; /// 控制符 
    else
    if(c == 32 || c == 9 || c == 10 || c == 13) return SpaceChar; /// 空白符 
    else
    if(Rin(33, c, 47) || Rin(58, c, 64) || Rin(91, c, 94) || c == 96 || Rin(123, c, 126)) return Operator; /// 算符 
    else
    if(Rin(48, c, 57)) return NumeralChar; /// 數字 
    else
    if(Rin(65, c, 90)) return CapitalChar; /// 大寫 
    else
    if(Rin(97, c, 122)) return LowerChar; /// 小寫 
    else
    if(c == 95) return UnderLine; /// 下劃線 
    else
    return -1; /// 理論上不會出現找不到字符分類的情況 
}

驗證這個程序的正確性,儘管這麼智障的程序,我們也要嚴謹。(驗證程序略)

II.組詞規則

由於我懶得學習正則表達式,組詞規則將用自然語言或代碼描述,儘管自然語言描述肯能並不嚴謹。

  1. 標識符組詞規則:
    意義:標識符組詞規則,規定了關鍵字、以及用戶自己定義的函數名、變量名、常量名、結構體名的格式標準。
    內容:標識符是一個以下劃線、小寫字母或大寫字母開頭的字符串,字符串中可以包含下劃線、小寫字母、大寫字母和數字,而不能此外的其他字符。
    標識符之後的第一個字符只能是空白符、算符或文件結尾。
  2. 十進制整數組詞規則:
    十進制整數是一個以數字爲開頭的字符串,字符串中只可以包含數字
    十進制整數之後的第一個字符只能是空白符、算符(除點號(即ASC(46))外)或文件結尾。
  3. 十進制實數組詞規則:
    十進制實數是一個以數字爲開頭的字符串,字符串中可以包含數字和小數點(即ASC(46)),而不能包含其他字符,且小數點在字符串中出現且僅出現一次
    十進制實數之後的第一個字符只能是空白符、算符(除點號(即ASC(46))外)或文件結尾。
  4. 十進制科學計數法組詞規則:
    十進制科學計數法是一個字符串,這個字符串可以無隙拆分爲首尾順次相接的三個非空子串。它們依次具有以下性質:第一部分,符合十進制實數組詞規則。第二部分只有六種可能,它們是 “e”,“e+”,“e-”,“E”,“E+”,“E-”(不含雙引號)。第三部分,符合十進制整數組詞規則。
    十進制科學計數法之後的第一個字符只能是空白符、算符(除點號(即ASC(46))外)或文件結尾。
  5. 十六進制整數的組詞規則:
    十六進制整數是一個以0x爲前綴的、長度大於等於3的字符串,除前兩個字符外,這個字符串剩餘的部分只能包含以下的22種字符:數字,小寫字母a~
    f,大寫字母A~F

    十六進制整數之後的第一個字符只能是空白符、算符(除點號(即ASC(46))外)或文件結尾。
  6. 常量字符串組詞規則:
    常量字符串是一個長度大於等於2的字符串,這個字符串的第一個字符和最後一個字符都是英文雙引號(即ASC(34)),其餘字符可以是除雙引號、回車符(即ASC(13))、換行符(即ASC(10))和文件尾(即ASC(-1))外的任何字符。
  7. 運算符組詞規則:
    運算符是一個字符串,若這個字符串是以下21個字符串之一,則它符合算符組詞規則:( ) [ ] . { } ; + - * / % = > >= < <= ^ == <>。(確實是21個,如果你能數明白,說明你能理解我的意思。)(同時注意區分“算符”和“運算符”的概念。)
  8. 註釋組詞規則:
    註釋是一個長度大於等於4的字符串,“/*”是這個字符串的一個前綴,“*/”是這個字符串的一個後綴,除了字符串的最後兩個字符外,這個字符串中不含有子串“*/”。
  9. 空白組詞規則:
    空白是一個非空字符串,這個字符串中只能含有空格(ASC(32))和Tab(ASC(9))兩種字符。(注意區分“空白”和“空白符”的涵義。)
    要求空白後的第一個字符不能是空白符。
  10. 換行組詞規則:
    換行是一個字符串,這個字符串可以無隙拆分成兩部分。第一部分,符合空白組詞規則。第二部分只有三種可能性,“\13\10”,“\13”,“\10”。(p.s.:“\x(x爲整數)”表示ASCII碼爲x的字符。)(注意區分“換行”與“換行符”的涵義。)
    對換行後的第一個字符要求比較特殊,若換行的最後一個字符爲換行符(ASC(10)),對其後的第一個字符沒有要求。若換行的最後一個字符爲回車(ASC(13)),則要求換行後的第一個字符不能爲換行符(ASC(10))。
  11. 文件結尾組詞規則:
    一個僅含一個字符的字符串,且這個字符爲EOF(即ASC(-1))。

III.詞法中用到的程序段

不難發現,以上詞法分析過程可以通過手動構造自動機輕鬆實現(我說我不想寫AC自動機,不代表我不手動構造自動機)。

先給出手稿,具體信息留坑待補:

詞法分析機

這個詞法分析自動機的正確性、嚴謹性是有待考證的,因此先不給出自動機的數據。(tips:有的else邊可能沒寫上。)

a. 字符退還功能的實現

寫一個字節的僞緩衝區即可。

char localTmp;              /// 僞緩衝區 
bool localTmpHasValue = 0;  /// 表明僞緩衝區中是否有字符 
char GetChar(FILE* fpin) {  /// 從文件中讀取一個字符(考慮僞緩衝區)  
    if(localTmpHasValue) {  /// 僞緩衝區非空 
        localTmpHasValue = false; /// 清空僞緩衝區 
        return localTmp;          /// 返回僞緩衝區的值 
    }else {
        localTmp = fgetc(fpin);
        return localTmp;          /// 在文件中重新讀取一個字節 
    }
}
void BackChar() { /// 將最後一次讀出的字符退還給緩衝區 
    if(localTmpHasValue) {
        fprintf(stderr, "緩衝區- 錯誤:不能連續向緩衝區退還兩個字符 ...");
        while(1);
    }else {
        localTmpHasValue = 1; /// 退還最後一次讀出的字符 
    }
}

b.得到的詞法單元的表示方式

我們可以用一個三元組來描述得到的詞法單元:(組詞規則編號,串長度,原內容字符串)。實際上,在詞法分析過程中,我們可以丟棄註釋和空白。但我們不能丟棄換行,因爲換行決定着當前光標在文件中所處的行數,行數在錯誤信息的輸出中有重要意義。

c.自動機的描述方式

對自動機的描述可以分爲兩部分,對點的描述,對邊的描述。

  1. 對點的描述:
    結點主要分爲四類:普通結點(0)、報錯結點(1)、接收結點(2)、退還接收結點(3)
    報錯結點:需要註明錯誤原因,輸出錯誤原因時需要輸出出錯位置。錯誤位置可用如下三元組描述:(當前文件名,行數,出錯前掃描到的最後一個字符)。
    接收結點/退還接受結點:需要註明接收到的字符串的類型(也就是採用了哪種組詞規則)。
    同時,我們還需要爲自動機的每一個結點分配一個獨一無二的編號。
  2. 對邊的描述:
    邊需要記錄三個信息,起點、終點以及能與這條邊匹配的字符集合。同點一樣,我們也需要爲自動機的每一條邊分配一個互不重複的編號。爲了便於描述每條邊所能匹配的字符集合,我們規定了以下幾個集合:
編號 集合名稱 集合內容
10 CLU CapitalCharLowerCharUnderLineCapitalChar \cup LowerChar \cup UnderLine
11 SP {32,9}\{32,9\}
12 N NumeralChar完全相同
13 NZ [49,57][49,57] (即非零數字)
14 SOE SpaceCharOperatorEndOfFile{46}SpaceChar\cup Operator \cup EndOfFile - \{46\}
15 dot {46}\{46\}
16 NAF [48,57][97,102][65,70][48,57]\cup [97,102] \cup [65,70]
17 OUS {40,41,91,93,46,123,125,59,43,45,42,94,37}\{40, 41, 91, 93, 46, 123, 125, 59, 43, 45, 42, 94, 37\}

簡單解釋一下這樣定義的原因:

集合名稱 解釋
CLU 標識符組詞規則中,第一個字符必須從屬於這個集合。
SP 空白組詞規則中,所有字符必須從屬於這一集合。
N 十進制整數,十進制實數組詞規則中經常出現這一集合中的字符。
NZ 十六進制數以“0x”爲前綴,需要特殊考慮字符“0”。
SOE 很多組詞規則,要求組詞結束後的第一個字符屬於這一集合。(注:這一集合並不包含dotASC(46)))
dot 這個集合沒必要定義,我純屬閒的。其中只含有一個字符——點(也就是英文句號)。
NAF 數字、a~f、A~F,這是十六進制數中常出現的元素集合。
OUS Operators which are Used Separately。在運算符組詞規則中,只能獨自構成運算符的字符。它們是 ( ) [ ] . { } ; + - * ^ %

我們用符號else表示一個特殊的集合,對於某一個結點,這個結點最多隻有一條標有else的出邊,表示如果這個結點的其他出邊都不能與當前輸入的字符匹配,那麼,它能夠與標有else的這條出邊匹配。

(有幾種算符可以與其他算符構成運算符,比如“>”、“=”和“<”,它們可以構成“>=”、“<=”、“==”和“<>”四種長度爲兩個字節的運算符。同時,它們自身也可以單獨構成運算符,因此需對它們逐個單獨考慮。)

int CheckCharType(int id, char c) { /// 判斷字符是否屬於某一集合 
    if(Rin(1, id, 9)) {
        return GetCharType(c) == id; /// 如果被判斷集合爲 9個基本字符集合 
    }else {
        int charType = GetCharType(c); /// 得到基本集合信息 
        #define cT charType
        if(id == 10) return cT == CapitalChar || cT == LowerChar || cT == UnderLine; /// _CLU
        else
        if(id == 11) return c == 32 || c == 9; /// _SP
        else
        if(id == 12) return cT == NumeralChar; /// _N
        else
        if(id == 13) return Rin(49, c, 57); /// _NZ
        else
        if(id == 14) return (cT == SpaceChar || cT == Operator || cT == EndOfFile) && c != 46; /// _SOE
        else
        if(id == 15) return c == 46; /// _dot
        else
        if(id == 16) return Rin(48, c, 57) || Rin(97, c, 102) || Rin(65, c, 70); /// _NAF
        else
        if(id == 17) return c==40 ||c==41||c==91||c==93||c==46||c==123||
                           c==125||c==59||c==43||c==45||c==42||c==94 ||c==37; /// _OUS
        else {
            return -1; /// error 可能是集合 id非法 
        }
        #undef cT
    }
}

根據以上的內容,在此制定用文本文件描述自動機信息的格式:

  • 第一行寫有兩個用空格分隔的十進制非負整數 N,MN, M,分別表示這個自動機的所有點的編號的集合爲V={1,2,..,N}V=\{1, 2, .., N\},所有邊的編號的集合爲 E={1,2,..,M}E=\{1, 2, .., M\}
  • 接下來的 NN 行,每行用於描述自動機中的一個結點。(爲了便於糾錯)行首包含一個正整數vvvVv \in V,且每行讀入的vv互不相同),表示當前被描述結點的編號。其後用空格隔開,有一個正整數ttt=0t=0表示當前結點爲普通結點,t=1t=1表示當前結點爲報錯結點,t=2t=2表示當前結點爲接收結點,t=3t=3表示當前結點爲退還接收結點,同時要求 t{0,1,2,3}t\in\{0, 1, 2, 3\}。若t=1t=1其後用一個字符串描述錯誤信息,方便起見,要求錯誤信息中不含空白符。若t=23t=2或3其後用一個整數描述接收到的字符串符是按照哪一種組詞規則接收的。
  • 接下來的 MM 行,每行用於描述自動機中的一條邊。(爲了便於糾錯)行首包含一個正整數eeeEe \in E,且每行讀入的ee互不相同),表示當前被描述的邊的編號。其後,有兩個用空格隔開的正整數p,qp,q,表示這條邊由pp指向qq(其中 pV,qVp\in V, q\in V)。再後,有一個字符串ii。若字符串ii符合十進制整數組詞規則,我們認爲這條邊能夠匹配以ii的數值爲ASCII碼的那個字符。若字符串ii"CLU","SP","N","NZ","SOE","dot","NAF","OUS","else"這九個字符串之一(不含雙引號和逗號),則認爲這條邊能夠匹配,以字符串ii爲名字的字符集合中的任意一個字符。

d.字典樹模板類的實現

這種字典樹在整個解釋器的實現過程中有着很多的用處,在這裏,我們用它來實現集合名字符串與集合編號的轉換。


template<class _T, int MaxNodeCnt>
class Tire { /// 字典樹模板類 字符串到 _T類型的映射 
    /// 採用兄弟節點表示法 (#0點爲根) 
    int  sSon[MaxNodeCnt]; /// 最後一個加入的兒子 
    int  sBro[MaxNodeCnt]; /// 年齡最小的兄弟 
    char fChr[MaxNodeCnt]; /// 父親邊能夠識別的字符 
    bool wVal[MaxNodeCnt]; /// 是否帶有值 
    _T   tVal[MaxNodeCnt]; /// 所帶有的值 
    int  nCnt; /// 當前節點數目 
    void Clear() { /// 清空字典樹 
        nCnt = 0;  /// 當前只有根節點 
        sSon[0] = -1;
        sBro[0] = -1; /// 根節點沒有兄弟和兒子 
        wVal[0] = 0;  /// 不帶有值 
    }
    int NewNode() { /// 新申請一個節點 
        if(nCnt == MaxNodeCnt - 1) { /// 字典樹已滿 
            fprintf(stderr, "字典樹- 錯誤:字典樹已滿");
            while(1);
        }
        nCnt ++; /// 新申請一個結點
        sSon[nCnt] = -1;
        sBro[nCnt] = -1;
        wVal[nCnt] = 0;   /// 不帶有值  
        return nCnt; 
    }
    void AddSon(int f, int s, char c) { /// 給 f添加一個兒子 s 
        sBro[s] = sSon[f];
        sSon[f] = s;       /// 承認父子關係 
        fChr[s] = c;       /// 父親邊能識別的字符 
    }
    public:
        Tire() {
            Clear();
        }
        void ClearTire(){ /// 清空字典樹 
            Clear();
        }
        bool Exist(const char* str) { /// 判斷字符串存在於定義域中 
            int rtn = 0; /// 當前根節點 
            for(int i = 0; str[i] != 0; i ++) { /// 掃描字符串 str 
                char cn  = str[i]; /// 當前字符 
                bool suc = false;  /// 記錄是否匹配成功 
                for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次訪問所有兒子 
                    if(fChr[v] == cn) { /// 匹配成功 
                        rtn = v; /// 進入子樹 
                        suc = true; /// 匹配成功 
                        break;
                    }
                }
                if(!suc) {
                    return false;
                }
            }
            return wVal[rtn];
        }
        _T GetValue(const char* str) { /// 判斷字符串存在於定義域中 
            int rtn = 0; /// 當前根節點 
            for(int i = 0; str[i] != 0; i ++) { /// 掃描字符串 str 
                char cn  = str[i]; /// 當前字符 
                bool suc = false;  /// 記錄是否匹配成功 
                for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次訪問所有兒子 
                    if(fChr[v] == cn) { /// 匹配成功 
                        rtn = v; /// 進入子樹 
                        suc = true; /// 匹配成功 
                        break;
                    }
                }
                if(!suc) {
                    return tVal[0];
                }
            }
            return tVal[rtn];
        }
        void SetValue(const char* str, _T newValue) {
            int rtn = 0; /// 當前根節點 
            for(int i = 0; str[i] != 0; i ++) { /// 掃描字符串 str 
                char cn  = str[i]; /// 當前字符 
                bool suc = false;  /// 記錄是否匹配成功 
                for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次訪問所有兒子 
                    if(fChr[v] == cn) { /// 匹配成功 
                        rtn = v; /// 進入子樹 
                        suc = true; /// 匹配成功 
                        break;
                    }
                }
                if(!suc) { /// 未成功匹配 
                    int s = NewNode(); /// 新申請結點 
                    AddSon(rtn, s, cn); /// 建立對應結點 
                    rtn = s;
                }
            }
            wVal[rtn] = true;
            tVal[rtn] = newValue; /// 設立新值 
        }
        void Erase(const char* str) { /// 刪除字符串 
            int rtn = 0; /// 當前根節點 
            for(int i = 0; str[i] != 0; i ++) { /// 掃描字符串 str 
                char cn  = str[i]; /// 當前字符 
                bool suc = false;  /// 記錄是否匹配成功 
                for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次訪問所有兒子 
                    if(fChr[v] == cn) { /// 匹配成功 
                        rtn = v; /// 進入子樹 
                        suc = true; /// 匹配成功 
                        break;
                    }
                }
                if(!suc) { /// 找不到對應的字符串 
                    return ;
                }
            }
            wVal[rtn] = false;
        }
};

手造數據測試字典樹的正確性:


Tire<int, 100> test; ///驗證字典樹的正確性 
int main() {
    while(1) {
        int opt; char s[10]; int val;
        printf("--> ");
        scanf("%d", &opt);
        if(opt == 1) { /// 修改/插入一個值 
            scanf("%s%d", s, &val);
            test.SetValue(s, val);
        }else if(opt == -1) { /// 刪除一個值 
            scanf("%s", s);
            test.Erase(s);
        }else if(opt == 0) { /// 查詢一個值 
            scanf("%s", s);
            if(!test.Exist(s)) {
                printf("Not Exist\n");
            }else {
                printf(" = %d. \n", test.GetValue(s));
            }
        }else {
            printf("opt error\n");
        }
    }
    return 0;
}

構造用於識別字符集合的字典樹:

Tire<int, 30> ClassId; /// 字符集合名稱編號轉換 
void InitClassId() {   /// 初始化這個集合 
    ClassId.SetValue("CLU",  10);
    ClassId.SetValue("SP" ,  11);
    ClassId.SetValue("N"  ,  12);
    ClassId.SetValue("NZ" ,  13);
    ClassId.SetValue("SOE",  14);
    ClassId.SetValue("dot",  15);
    ClassId.SetValue("NAF",  16);
    ClassId.SetValue("OUS",  17);
    ClassId.SetValue("else", -1);
}

e.自動機模板類的實現

不像Tire模板類在很多地方都能派上用場,自動機(有限狀態機)模板類只在詞法分析過程中起重要作用。

template<int MaxNodeCnt>
class autoMachine { /// 自動機模板類 
    int N, M; /// 自動機的點數、邊數 
    int  typ[MaxNodeCnt]; /// 記錄結點是哪一種結點 
    int  nxt[MaxNodeCnt][256]; /// 記錄鄰接矩陣 
    char msg[MaxNodeCnt][256]; /// 記錄錯誤信息 
    int  mtd[MaxNodeCnt]; /// 接收結點 的 組詞規則 
    int  els[MaxNodeCnt]; /// 記錄 els邊的指向 
    int now; /// 現在所在的結點 
    void Clear() {
        memset(nxt, 0xff, sizeof(nxt)); /// nxt 初始化爲 -1 
        memset(typ, 0xff, sizeof(typ)); /// typ 初始化爲 -1 
        memset(els, 0xff, sizeof(els)); /// els 初始化爲 -1 
    }
    void Link(int f, int t, unsigned char c, int eId) { /// 連接兩點(單邊) 
        if(nxt[f][c] == -1) {
            nxt[f][c] = t;
        }else {
            fprintf(stderr, "自動機- 錯誤:邊的重複連接, eId = %d\n", eId);
            while(1);
        }
    }
    void AddEdge(int f, int t, const char* val, int eId) { /// 從 f向 t連邊(可能是集合邊) 
        if(Rin('0', val[0], '9') || val[0] == '-') { /// val中存着數字 
            int c;
            sscanf(val, "%d", &c); /// 讀入這個數字 
            Link(f, t, c, eId);
        }else { /// val中存放着集合名 
            if(!ClassId.Exist(val)) { /// 集合名不存在 
                fprintf(stderr, "自動機- 錯誤:集合名(%s)不存在 eId = %d\n", val, eId);
            }else {
                int id = ClassId.GetValue(val); ///得到集合 id 
                if(id == -1) { /// else 邊 
                    for(int c = 0; c <= 255; c ++) { /// 枚舉出邊 
                        if(nxt[f][c] == -1) {
                            Link(f, t, c, eId); /// 連接所有未連之邊 
                        }
                    }
                }else {
                    for(int c = 0; c <= 255; c ++) {
                        if(CheckCharType(id, c)) { /// 連接集合規定的所有邊 
                            Link(f, t, c, eId);
                        }
                    }
                }
            }
        }
    }
    bool CheckNodeFull(int id) { /// 檢測某個點是否有全部出邊 
        for(int i = 0; i <= 255; i ++) {
            if(nxt[id][i] == -1) return false; /// 有空出邊 
        }
        return true;
    }
    bool StrSame(const char* s1, const char* s2) {
        for(int i = 0; ; i ++) {
            if(s1[i] != s2[i]) return false;
            if(s1[i] == 0) break;
        }
        return true;
    }
    public:
        int line; /// 文本中所處的行數 
        void ClearAutoMachine() {
            Clear();
            line = 0;
        }
        AutoMachine() {
            ClearAutoMachine();
        }
        void ReStart(int startNode = 1) { /// 自動機的起始狀態 
            now = startNode;
        }
        bool CheckFull() { /// 檢測是否每個普通結點都有全部出邊 
            bool flag = true;
            for(int i = 1; i <= N; i ++) {
                if(!typ[i] && !CheckNodeFull(i)) { /// 檢測每個普通節點是否有全部出邊 
                    flag = false;
                    fprintf(stderr, "自動機- 警告:普通結點 i = %d 出邊不完備\n", i);
                }
            }
            if(!flag) {
                fprintf(stderr, "自動機- 錯誤:自動機出邊不完備");
                while(1);
            }
            return flag;
        }
        void InputFromFile(const char* filepath) { /// 從文件讀入自動機 
            FILE* fpin = fopen(filepath, "r");
            if(fpin == NULL) {
                fprintf(stderr, "自動機- 錯誤:指定文件(%s)不可讀入", filepath);
                while(1);
            }
            fscanf(fpin, "%d%d", &N, &M); /// 輸入自動機的點數和邊數 
            for(int i = 1; i <= N; i ++) { /// 輸入點的信息 
                int v;
                fscanf(fpin, "%d", &v); /// 輸入當前被描述點的編號 
                if(typ[v] != -1) {
                    fprintf(stderr, "自動機- 錯誤:點信息的重複描述(點信息- 第 %d 行) v = %d", i, v);
                    while(1);
                }
                int t;
                fscanf(fpin, "%d", &t);
                if(!Rin(0, t, 3)) {
                    fprintf(stderr, "自動機- 錯誤:結點類型描述不合法(點信息- 第 %d 行) v = %d", i, v);
                    while(1);
                }
                typ[v] = t;
                if(t == 1) { /// 報錯結點 
                    fscanf(fpin, "%s", msg[v]); /// 輸入報錯信息 
                }else if(t != 0){
                    fscanf(fpin, "%d", &mtd[v]); /// 輸入組詞規則編號 
                }
            }
            for(int i = 1; i <= M; i ++) { /// 輸入邊信息 
                int e;
                fscanf(fpin, "%d", &e); /// 輸入邊的編號 (沒用) 
                int p, q;
                fscanf(fpin, "%d%d", &p, &q); ///輸入所連接的點 
                char I[256];
                fscanf(fpin, "%s", I); /// 輸入集合 
                if(StrSame(I, "else")) { /// else邊 必須最後插入 
                    if(els[p] != -1) {
                        fprintf(stderr, "自動機- 錯誤:點 p = %d, 被插入兩條else邊, eId = %d", p, e);
                        while(1);
                    }else {
                        els[p] = q;
                    }
                }else AddEdge(p, q, I, e);
            }
            for(int i = 1; i <= N; i ++) { /// 插入所有else邊 
                if(els[i] != -1)
                    AddEdge(i, els[i], "else", -1); /// eId散軼 
            }
        }
        int PushForward(unsigned char c, int& mTD) { /// 自動機狀態 向前進 
            now = nxt[now][c]; /// 向前進 
            if(now == -1) {
                fprintf(stderr, "自動機- 錯誤:進入虛空節點"); /// 可能是接受字符串後忘記 ReStart導致 
                while(1);
            }
            if(typ[now] == 1) { /// 報錯 
                fprintf(stderr, "自動機- 信息:報錯結點 now = %d:%s, 行數 %d\n", now, msg[now], line);
                while(1);
            }else if(typ[now]) { /// mTD 接收組詞規則 
                mTD = mtd[now];
            }
            return typ[now]; /// 返回當前結點類型 
        }
};

給出一個自動機文件:

44 69
 1 0
 2 0
 3 1 未封閉的雙引號
 4 1 正文中的非法字符
 5 2 6
 6 0
 7 0
 8 0
 9 2 8
10 3 7
11 1 未封閉的多行註釋
12 0
13 3 1
14 1 標識符中的非法字符
15 0
16 0
17 1 數值中的非法字符
18 3 5
19 0
20 0
21 0
22 0
23 3 4
24 3 2
25 1 數值中的非法字符
26 3 3
27 1 科學計數法中的非法字符
28 2 7
29 0
30 2 7
31 0
32 2 7
33 3 7
34 3 7
35 0
36 0
37 3 10
38 2 10
39 2 7
40 3 9
41 0
42 2 7
43 3 7
44 2 11
 1  1  4 else
 2  1  2 34
 3  2  2 else
 4  2  3 -1
 5  2  3 13
 6  2  3 10
 7  2  5 34
 8  1  6 47
 9  6  7 42
10  7  7 else
11  7  8 42
12  8  9 47
13  1 12 CLU
14  6 10 else
15  8  7 else
16  8 11 -1
17  7 11 -1
18  1 15 48
19 12 12 CLU
20 12 12 N
21 12 13 dot
22 12 13 SOE
23 12 14 else
24 15 17 else
25 15 16 120
26 16 16 NAF
27 16 17 else
28 16 18 SOE
29  1 19 NZ
30 15 19 N
31 15 24 SOE
32 19 24 SOE
33 19 19 N
34 19 20 dot
35 19 25 else
36 20 20 N
37 20 25 else
38 20 26 SOE
39 20 21 69
40 20 21 101
41 21 27 else
42 21 22 N
43 21 22 45
44 21 22 43
45 22 22 N
46 22 23 SOE
47 22 27 else
48  1 28 OUS
49  1 29 62
50 29 30 61
51 29 33 else
52  1 31 61
53 31 32 61
54 31 34 else
55  1 41 60
56 41 39 61
57 41 42 62
58 41 43 else
59  1 44 -1
60  1 35 SP
61  1 36 13
62  1 38 10
63 35 35 SP
64 35 36 13
65 36 37 else
66 36 38 10
67 35 40 else
68 35 38 10
69 15 20 dot

給出這個自動機文件對應的手稿:

(留坑待補)

給出測試時用的代碼:

autoMachine<50> LexicalAnalysis; /// 詞法分析機 
char tmpStr[1024]; /// 詞法分析緩衝區 
int  charCnt;      /// 緩衝區中字符數量  
void LAtest(const char* filepath) { /// 詞法分析測試 
    FILE* fpin = fopen(filepath, "r");
    if(fpin == NULL) {
        fprintf(stderr, "詞法分析測試- 錯誤:指定分析文件(%s)不可讀", filepath);
        while(1);
    }
    localTmpHasValue = 0; /// 清空緩衝區 
    #define LA LexicalAnalysis
    LA.ReStart(1); /// 開始讀入 
    int mtd = 0;   /// 最後一次接收時採用的組詞規則 
    bool isEnd = 0; /// 記錄是否讀到文件尾 
    do {
        char cn = GetChar(fpin);
        tmpStr[charCnt ++] = cn; /// 讀入一個字符 
        int stat = LA.PushForward(cn, mtd); /// 當前狀態 
        if(stat) { /// 可以接收 
            if(stat == 2) {
                tmpStr[charCnt] = 0;
                if(mtd!=10 && mtd!=9) /// 不顯示換行和空白 
                    printf("%d {%s}\n", mtd, tmpStr);
                charCnt = 0;
                LA.ReStart(1); /// 一定要記得每次接收後重新開始自動機 
            }else if(stat == 3) {
                BackChar(); charCnt --; /// 退還最後一個字符 
                tmpStr[charCnt] = 0;
                if(mtd!=10 && mtd!=9) 
                    printf("%d {%s}\n", mtd, tmpStr);
                charCnt = 0;
                LA.ReStart(1); /// 一定要記得每次接收後重新開始自動機
            }
            if(mtd == 11) isEnd = 1;
            if(mtd == 10) {
                LA.line ++;
                printf(" LA.line = %d\n", LA.line);
            }
        }
    }while(!isEnd);
    #undef LA
}
int main() {
    InitClassId();
    LexicalAnalysis.ClearAutoMachine();
    LexicalAnalysis.InputFromFile("auto.txt");
    LexicalAnalysis.CheckFull();
    LAtest("in.txt");
    return 0;
}

用於測試的一個小文件:

Func IsPrime(Int x) As Int
    If(x<=1){ /*1不是質數*/
        Return 0;
    }else{
        If(x==0x02){ /*2是質數*/
            Return 1;
        }
        For(Int i=2; i*i<=x; i ++){
            If(x%i==0)Return 0;
        }
        Return 1;
    }
EndFunc

1.0
0.5
1.0e-6
"Hello world!"

2.語法分析

按照既定的語法,將詞法分析得到的散亂的詞彙整理成抽象語法樹的形式。在這個過程中,同時檢驗源代碼的語法是否正確。形成抽象語法樹之後,我們還將把這門編程語言轉化成一門類似彙編語言的中間語言。

爲了清除的描述語法,我們將規定一些詞彙集合,從而輔助我們定義語法。

I.詞彙集合

a.保留字集合

保留字是字符串,所有保留字都符合標識符組詞規則(定義見語法分析)。這門語言中保留字列舉如下:

if      else
for     while   break    continue
and     or      not
return
int     double
struct

b.用戶定義標識符集合

除保留字外,其餘所有符合標識符組詞規則的詞彙,稱爲用戶定義標識符集合。

c.常量集合

符合 十進制整數組詞規則、十進制實數組詞規則、十進制科學計數法組詞規則、十六進制整數組詞規則、常量字符串組詞規則 五種規則中任意一種規則的詞彙,稱爲常量。

II.定義語法

a.常量的定義

單個常量的定義,有以下三種形式:

const [int/double] [變量名1] = [值1] ;

const [int/double] [數組名][維度1長度][維度2長度]... = { [值1], [值2], [值3], ... } ;

const [結構體名] [變量名] = { [值1], [值2], [值3], ... } ;

類型相同的多個常量可由逗號分隔後,用同一個語句定義:

const [類型名] [描述1], [描述2], [描述3], ... ;

未完待續 …

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