開篇
編譯,簡單的說,就是把源程式轉換為可執行程式。從hello world 說程式執行機制 裡面簡單的說明了程式執行的過程,以及一個程式是如何一步步變成可執行檔案的。在這個過程中,編譯器做了很多重要的工作。對底層該興趣的我,自然的,也就迫切想搞清楚編譯的內部實現,也就是編譯的原理。
這篇文章主要說的是編譯器前端,詞法分析器的原理,最後會給出一個詞法分析器的簡單實現。
介紹
編譯簡單的說,就是把源程式轉化為另一種形式的程式,而其中關鍵的部分就是理解源程式所要表達的意思,才能轉化為另一種源程式。
可以用一個比喻來說明問題:人A和人B想要交談,但是他們都不知道彼此的語言,這就需要一個翻譯C,同時懂得A和B的語言。有了C做中間層,A和B才能正常交流。C的作用就有點像編譯器,它必須能理解源程式所要表達的意思,才能把資訊傳遞給另一個。
編譯器也一樣,它的輸入是語言的原始檔(一般可以是文字檔案)對於輸入的檔案,首先要分離出這個輸入檔案的每個元素(關鍵字、變數、符號、、)
然後根據語言的文法,分析這些元素的組合是否合法,以及這些組合所表達的意思。
程式設計語言和自然語言不一樣,都是用符號來描述,每個特定的符號表示特定的意思,而且程式設計語言是上下文無關的。上下文無關就是某一個特定語句所要表達的意思和它所處的上下文沒有關係,只有它自身決定。
這篇博文主要說的就是詞法分析,也就是把輸入的符號串整理成特定的詞素。
詞法分析
定義:
詞法分析器的功能輸入源程式,按照構詞規則分解成一系列單詞符號。單詞是語言中具有獨立意義的最小單位,包括關鍵字、識別符號、運算子、界符和常量等
(1) 關鍵字 是由程式語言定義的具有固定意義的識別符號。例如,Pascal 中的begin,end,if,while都是保留字。這些字通常不用作一般識別符號。
(2) 識別符號 用來表示各種名字,如變數名,陣列名,過程名等等。
(3) 常數 常數的型別一般有整型、實型、布林型、文字型等。
(4) 運算子 如+、-、*、/等等。
(5) 界符 如逗號、分號、括號、等等。
輸出:
詞法分析器所輸出單詞符號常常表示成如下的二元式:
(單詞種別,單詞符號的屬性值)
單詞種別通常用整數編碼。識別符號一般統歸為一種。常數則宜按型別(整、實、布林等)分種。關鍵字可將其全體視為一種。運算子可採用一符一種的方法。界符一般用一符一種的方法。對於每個單詞符號,除了給出了種別編碼之外,還應給出有關單詞符號的屬性資訊。單詞符號的屬性是指單詞符號的特性或特徵。
示例:
比如如下的程式碼段:
while(i>=j) i–
經詞法分析器處理後,它將被轉為如下的單詞符號序列:
=, _>
詞法分析分析器作為一個獨立子程式
詞法分析是編譯過程中的一個階段,在語法分析前進行。詞法分析作為一遍,可以簡化設計,改進編譯效率,增加編譯系統的可移植性。也可以和語法分析結合在一起作為一遍,由語法分析程式呼叫詞法分析程式來獲得當前單詞供語法分析使用。
詞法分析器設計
輸入、預處理
詞法分析器工作的第一步是輸入源程式文字。在許多情況下,為了更好地對單詞符號識別,把輸入串預處理一下。預處理主要濾掉空格,跳過註釋、換行符等。
超前搜尋
詞法分析過程中,有時為了確定詞性,需超前掃描若干個字元。
對於FORTRAN 語言,關鍵字不作為保留字,可作為識別符號使用, 空格符號沒有任何意義。為了確定詞性,需超前掃描若干個字元。
在FORTRAN中
1 DO99K=1,10
2 IF(5.EQ.M) I=10
3 DO99K=1.10
4 IF(5)=55
這四個語句都是正確的語句。語句1和2 分別是DO和IF語句,語句3和4是賦值語句。為了正確區別1和3,2和4語句,需超前掃描若干個字元。
1 DO99K=1,10 2 IF(5.EQ.M) I=10
3 DO99K=1.10 4 IF(5)=55
語句1和3的區別在於符號之後的第一個界符:一個為逗號,另一個為句末符。語句2和4的主要區別在於右括號後的第一個字元:一個為字母,另一個為等號。為了識別1、2中的關鍵字,必須超前掃描多個字元。超前到能夠肯定詞性的地方為止。為了區別1和3,必須超前掃描到等號後的第一個界符處。對於語句2、4來說,必須超前掃描到與IF後的左括號相對應的那個右括號之後的第一個字元為止。
狀態轉換圖
詞法分析器使用狀態轉換圖來識別單詞符號。狀態轉換圖是一張有限方向圖。在狀態轉換圖中,有一個初態,至少一個終態。
其中0為初態,2為終態。這個轉換圖識別(接受)識別符號的過程是:從初態0開始,若在狀態0之下輸入字元是一個字母,則讀進它,並轉入狀態1。在狀態1之下,若下一個輸入字元為字母或數字,則讀進它,並重新進入狀態1。一直重複這個過程直到狀態1發現輸入字元不再是字母或數字時(這個字元也已被讀進)就進入狀態2。狀態2是終態,它意味著到此已識別出一個識別符號,識別過程宣告終止。終態結上打個星號意味著多讀進了一個不屬於識別符號部分的字元,應把它退還給輸入口中 。如果在狀態0時輸入字元不為“字母”,則意味著識別不出識別符號,或者說,這個轉換圖工作不成功。
正規表示式與正規集
正規表示式是說明單詞的一種重要的表示法(記號),是定義正規集的工具。在詞法分析中,正規表示式用來描述標示符可能具有的形式。
定義(正規式和它所表示的正規集):
設字母表為S,
1. e和Ø都是S上的正規式,它們所表示的正規集分別為{e}和{ };
2. 任何aÎ S,a是S上的一個正規式,它所表示的正規集為{a};
3. 假定U和V都是S上的正規式,它們所表示的正規集分別為L(U)和L(V),那麼,(U), U|V, U·V, U*也都是正規式,它們所表示的正規集分別為L(U), L(U)ÈL(V), L(U)L(V)和(L(U))*;
4. 僅由有限次使用上述三步驟而定義的表示式才是S上的正規式,僅由這些正規式所表示的字集才是S上的正規集。
正規式的運算子的“½”讀為“或” ,“· ”讀為“連線”;“*”讀為“閉包”(即,任意有限次的自重複連線)。在不致混淆時,括號可省去,但規定算符的優先順序為“(”、“)”、“*”、“· ”、“½” 。連線符“· ”一般可省略不寫。“*”、“· ”和“½” 都是左結合的。
例 令S={a,b}, S上的正規式和相應的正規集的例子有:
正規式 正規集
a {a}
a½b {a,b}
ab {ab}
(a½b)(a {aa,ab,ba,bb}
a * {e ,a,a, ……任意個a的串}
ba* {b, ba, baa, baaa, …}
(a½b)* {e ,a,b,aa,ab ……所有由a和b
組成的串}
(a½b)*(aa½bb)(a½b)* {S*上所有含有兩個相繼的a
或兩個相繼的b組成 的串}
定理:若兩個正規式U和V所表示的正規集相同,則說U和V等價,寫作U=V。
證明b(ab)*=( ba)*b
證明:因為L(b(ab)*)={b}{e, ab, abab, ababab, …}
={b, bab, babab, bababab, …}
L((ba)*b) ={e, ba, baba, bababa, …}{b}
={b, bab, babab, bababab, …}
= L(b(ab)*)
所以, b(ab)*=( ba)*b
設U,V,W為正規式,正規式服從的代數規律有:
(1) U½V=V½U (交換律)
(2) U½(V½W)=(U½V)½W (結合律)
(3) U(VW)=(UV)W (結合律)
(4) U(V½W)=UV½UW (V½W)U=VU½WU (分配律)
(5) eU=U e=U
分析器的簡單實現
上文主要介紹了詞法分析的一些相關的知識,而對詞法分析器的具體實現還沒有具體提到,為了能更好的理解詞法分析,我寫了一個簡單的詞法分析器。
雖然說是語法分析器,但實現的功能很簡單,只是對輸入的程式把註釋去掉,其中用到了上面關於狀態轉換圖部分的知識。
分析:
一般的程式設計語言, 註釋部分的形式為;
/* 註釋部分、、、、*/
我們的程式總是順序的一個一個字元讀取輸入檔案的。我們的目的是把註釋部分去掉,那麼對於輸入的字元流,我們只要識別出“/*”就知道後面的部分是註釋部分,直到識別輸入流中出現”*/”為止。
對字元流的處理是一個一個進行的,每讀入一個字元,就判斷,如果字元是“/”,就說明後面 的部分可能是註釋,再看下一個輸入字元,如果是“*”, 就是上面所說的情況:“ /*”那麼後面的部分就是註釋部分,然後再用相同的方法找出”*/”就可以了。
這個識別的過程就可以用狀態轉換圖來清晰的表示:
對於讀入的每個符號都要進行判斷,如果是“/”說明後面的部分有可能是註釋,進入狀態1。如果後面的輸入是“*”那麼就可以確定以後的內容為註釋內容,如果後面的輸入不是”*”,說明後面的內容不是註釋,前面出現的”/”可能是做除號使用,如“5/3”
其實上面的流程圖也就對應了程式實現的邏輯,可以用switch-case 來實現,對於每個輸入,判斷後跳轉到相應的狀態,然後繼續判斷。
下面是程式虛擬碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
while((ch=getchar())!=EOF) switch(state) case 1 :if ch=="/",state=2,break; case 2: if ch=="*",state=3 else state=1;break; case 3:.......... case 4:.......... |
詞法分析器
這個程式比較簡單,就不給出原始碼了。接下來是一個簡單的詞法分析器的程式碼,可以實現對關鍵字(如 while end if 等),對數字的識別,去掉空格符等。
下面是這個分析器的功能:
1、待分析的簡單語言的詞法
(1) 關鍵字:
begin if then while do end
所有關鍵字都是小寫。
(2)運算子和界符:
:= + – * / > >= = ; ( ) #
(3)其他單詞是識別符號(ID)和整型常數(NUM),通過以下正規式定義:
ID=letter(letter| digit)*
NUM=digit digit *
(4)空格由空白、製表符和換行符組成。空格一般用來分隔ID、NUM,運算子、界符和關鍵字,詞法分析階段通常被忽略。
2、 各種單詞符號對應的種別碼
詞法分析程式的功能
輸入:所給文法的源程式字串。
輸出:二元組(syn,token或sum)構成的序列。
其中:syn為單詞種別碼;
token為存放的單詞自身字串;
sum為整型常數。
下面是程式原始碼,基於上面的討論,應該比較好了解了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
#include #includestring.h> #includechar prog[80],token[8]; char ch; int syn,p,m=0,n,row,sum=0; char *rwtab[6]={"begin","if","then","while","do","end"}; void scaner() { /* 共分為三大塊,分別是標示符、數字、符號,對應下面的 if else if 和 else */ for(n=0;n8;n++) token[n]=NULL; ch=prog[p++]; while(ch==' ') { ch=prog[p]; p++; } if((ch>='a'&&ch'z')||(ch>='A'&&ch'Z')) //可能是標示符或者變數名 { m=0; while((ch>='0'&&ch'9')||(ch>='a'&&ch'z')||(ch>='A'&&ch'Z')) { token[m++]=ch; ch=prog[p++]; } token[m++]=''; p--; syn=10; for(n=0;n6;n++) //將識別出來的字元和已定義的標示符作比較, if(strcmp(token,rwtab[n])==0) { syn=n+1; break; } } else if((ch>='0'&&ch'9')) //數字 { { sum=0; while((ch>='0'&&ch'9')) { sum=sum*10+ch-'0'; ch=prog[p++]; } } p--; syn=11; if(sum>32767) syn=-1; } else switch(ch) //其他字元 { case'':m=0;token[m++]=ch; ch=prog[p++]; if(ch=='>') { syn=21; token[m++]=ch; } else if(ch=='=') { syn=22; token[m++]=ch; } else { syn=23; p--; } break; case'>':m=0;token[m++]=ch; ch=prog[p++]; if(ch=='=') { syn=24; token[m++]=ch; } else { syn=20; p--; } break; case':':m=0;token[m++]=ch; ch=prog[p++]; if(ch=='=') { syn=18; token[m++]=ch; } else { syn=17; p--; } break; case'*':syn=13;token[0]=ch;break; case'/':syn=14;token[0]=ch;break; case'+':syn=15;token[0]=ch;break; case'-':syn=16;token[0]=ch;break; case'=':syn=25;token[0]=ch;break; case';':syn=26;token[0]=ch;break; case'(':syn=27;token[0]=ch;break; case')':syn=28;token[0]=ch;break; case'#':syn=0;token[0]=ch;break; case'n':syn=-2;break; default: syn=-1;break; } } int main() { p=0; row=1; cout"Please input string:"endl; do { cin.get(ch); prog[p++]=ch; } while(ch!='#'); p=0; do { scaner(); switch(syn) { case 11: cout"("","")"break; case -1: cout"Error in row ""!"break; case -2: row=row++;break; default: cout"("","")"break; } } while (syn!=0); } |
改程式在C-free5上除錯通過
下面是程式截圖:
小結
這裡主要說的是編譯器中的詞法分析,還介紹了詞法分析的一些相關知識,最後給出了一個很簡單的詞法分析器的實現。
參看資料:編譯原理