1. 程式人生 > >現代編譯原理——第1章:詞法分析

現代編譯原理——第1章:詞法分析

 轉自: http://www.cnblogs.com/BlackWalnut/p/4467749.html 

  當我們寫好一份原始碼,提交給編譯器的時候,這是編譯器對我們提交程式碼進行詞法分析。這個整個編譯過程的第一步。詞法分析器將我們的提交的程式碼看作是一個文字,它工作的目的就是將這個文字中不符合我們所使用的語言(c++或者java)的單詞(字串)挑選出來,以及將符合語言的單詞(字串)進行分類。

  對於第一個功能,挑選不合格字串,我們可以這樣理解。例如,對於c++而言,定義變數名不能使用數字開頭,那麼如果出現 int  1tmp;語句就是非法的。

  對於第二個功能,我們一方面要確定怎麼解讀一個字串。例如,if32,是將其看成為if 和 32 ?還是將其看成if32?另一方面,我們要將其讀到的字串進行分類,例如,if 是內部關鍵字,它是屬於IF類.但是對於int tmp; 中的tmp,它對於語言(c++或者其他語言)而言,就是一個id,因此它屬於ID,而前面的int,則是INT.再比如對於43,它是一個常量,也是一個NUM,3.14則是一個REAL。

  那麼對於一段程式碼  int a = 10 , b = 20;  int c ; c = a + b ;  經過詞法處理後得到的輸出為

  INT ID ASSIGN NUM   COMMA INT ID  ASSIGN NUM  SEMI(;)  INT ID  SEMI  ID  ASSIGN  ID  PLUS  ID  SEMI

      上面的輸出的符號,將用於語法分析時使用。很顯然,語法分析只專注於分析一個單詞是否合法,以及分類,並不處理單詞與單詞之間的關係是否合法。例如,

  void  func() { // action} ;  int a = func ;

  對於第二個語句,詞法分析並不關心其是否正確,照樣輸出  INT ID ASSIGN ID SEMI.

  瞭解了詞法分析的作用後,乍一看,詞法分析面對的處理物件似乎是一個單詞,例如,先讀入一個單詞,判斷這個單詞的型別,再讀入一個,判斷型別,and so on。但是,如何判斷是否讀入了一個“正確”的單詞呢?例如 , c=a+b 和 c = a + b  兩個都是合法的c++語句。對於後者而言,可以用空格來表示讀完一個單詞,但是對於前者,詞法分析器是將c=a+b看成一個單詞呢?還是將c看成一個單詞 ?

  所以,詞法分析器是將一個字母作為分析目標,根據該字母的下一個字母來判斷是否應該判定該字母所屬型別。對於c=a+b而言,當讀入a時,詞法分析其將其標記為ID狀態,當讀入=號時,則表示a的狀態已經可以確定,則將a的狀態(ID)輸出。再舉個例子,tmp=c-d,當詞法分析器讀入t時,將其標記為ID狀態,讀入m和p時,狀態不變,當讀入=時,輸出當前狀態,並退回到其實狀態,然後對讀入的=進行分析。用畫圖來表示就是:

                (我保證這是我所有部落格中唯一一張手繪圖,務必珍惜。。。。。沒見過健身完手抖的麼)

  方框2就是狀態ID,狀態1時開始狀態,當讀入t時,到達狀態2.當讀入m時轉一圈回到2(上面標有m的帶箭頭的曲線,對的,曲線,不是頭上的犄角),讀入p時,也還回到狀態2。當讀入p時,發現沒有地方可以走了,就輸出狀態2,也就是ID,然後回到狀態1,如果狀態1有一條標有=的曲線指向另一個狀態,那麼就沿著那條曲線(我們成為邊)到另一個狀態。

  我們把上圖叫做一個狀態機,因為它的狀態時有限的,且只要讀進一個字元我們就能確定找到唯一一條邊,或者時找到一個狀態,所以我們成為確定有限自動狀態機(DFA)。

  這樣我們就知道每一個或者多個狀態確定一個語言中的一個分類(初始狀態除外),那麼對於ID狀態,c++中要求ID的不能用數字開頭,那麼就可以得到一下一個狀態機,

                           (咳咳。。。。額,大家時來學知識的,不要在意保證不保證的這些細節,這不重要)

  上圖就描述了一個確定有限自動狀態機,當在狀態1的時候,讀入任何以a-z和A-Z的字元都將進入狀態2,在狀態2中讀入任何0-9,a-z,A-Z,都將回到狀態2.如果讀入一個其他的字元,狀態機輸出狀態2(ID),然後回到狀態1.(趕緊隨便定義一個變數名試試吧)。

  這樣,只要將一個語言中的所有分類分別用一個DFA表示出來,再將各個DFA連線到一起,就可以對一個語言的原始碼進行分析了。例如以下:

  可以看出,這個大的狀態機並不是簡單的將幾個小的狀態機連線起來,還涉及到一些合併操作。

  有以上可以看出來,確定一個語言的狀態機,要解決兩個問題,第一,如何描述語言中的一個類別,例如 REAL,IF,ID。第二,如果將各個小的狀態機連線起來,組成一個大的語言確定有限狀態機。

  對於第一個問題,我們引入正則表示式(也稱作正規式)。正則表示式的規定如下:

  那麼,如果要描述ID ,則其正則表示式為[a-zA-Z][a-zA-Z0-9].正則表示式主要作用是使用flex詞法分析器生成器。

  對於第二問題,我們引入非確定有限狀態機(NFA)。相比於確定有限自動狀態機,非確定有限自動狀態機中有一條或者多條邊可以為空,或者有標有同一個符號的兩條指向不同狀態的邊。例如下面兩幅圖:

                            

  因為,當讀入一個數據的時候,我們無法確定到底選擇哪一條邊,所以,是非確定的。

  那麼,將一個正則表示式轉化成為一個確定有限狀態機的過程為:正則表示式轉化為非確定有限狀態機,非確定有限狀態機轉化為確定有限狀態機。

     正則表示式轉化為非確定有限狀態機,可以使用下圖:

  

  這樣將語言中的所有類分別使用正則表示式表示出來,然後將每個正則表示式表示成一個小的非確定有限制狀態機,然後使用一個新的狀態作為起始狀態,用空邊將該起始狀態和其他小的非確定有限狀態機的起始狀態連線起來,就得到該語言的非確定有限狀態機。

  為了將非確定有限狀態機轉化為確定有限狀態機,我們引入ε閉包的概念:將狀態集合A(包含一個或者多個狀態)和使用空邊和A連線的狀態組成的新的狀態集合,得到的新的狀態集合成為closure(A),那麼就有如下等式:

                                             

  等式左邊表示為:狀態集合d吃掉一個標示符c後能夠到達的狀態的新狀態集合(假設為)h。注意到因為有closure,所以h還包含使用空邊和h中所有狀態連線的狀態。edge(s,c)標示從s出發,吃掉c後能到達的所有狀態的集合。

  我們根據以上等式,將下面的左圖(NFA)轉化為右圖(DFA):

  上圖轉化描述為:對於左圖狀態1,其ε閉包為A(1,4,9,14),就是右圖中的開始狀態集合。可以看出,這個集合A可以吃掉的字元包括i,a-z,0-9,以及任何字元(就是A中所有狀態能夠吃掉的字元的集合),如果A吃掉i(就是對A中的各個狀態s求edge(s,i)),那麼可以到達的集合為B`(2,5,15),再求closure(B `),則得到新的集合B(2,5,6,8,15)。然後考慮A吃掉(a-h,j-z),然後求閉包,那麼得到集合C(5,6,8,15),等等。直到將A可以吃掉的字元都計算過後,我們就得到了上右圖中,和起始狀態集合連線的各個狀態集合。然後依次再對其他狀態集合進行相同操作。那麼最終得到的式右圖。這樣就完成了NFA到DFA的轉化。

  如果將右圖中的每個狀態集合看成一個狀態,那麼就得到了一個確定有限自動狀態機。那麼,對於右圖每個狀態(或者狀態集合),我們如何確定這個狀態(狀態集合)代表的是語言中的哪個類呢?也就是如何確定右圖中的每個狀態的上角標,如ID,NUM,IF等。這裡有三個原則需要遵守:

  第一:最長字串原則。當我們遇到例如 if32i 這種字串時,我們將對整個字串進行匹配,而不是匹配到if就返回型別IF。

  第二:終止狀態優先原則。在我們從NFA轉化到DFA過程中,如果一個狀態集合包含終止狀態,則在轉化後得到的DFA中,該狀態集合為終止狀態集合。

  第三:規則優先原則。如果轉化後的狀態集合包含多個終止狀態,例如狀態集合B中包含8和15,那麼指定一個具有更高的規則優先順序,在上面的例子中,我們指定8的規則優先順序高,那麼最終B代表的是ID類。那麼在程式中如何指定呢?就是使用flex過程中,定義越靠前的正則表示式規則優先順序越高。至於flex怎麼使用,這不是我們的重點,可以參考其他資源。

  好了,以上就是所有的詞法分析過程,其實,最終我們是將一個DFA轉化為一個二維陣列,如下:

                                                              

  這是一個DFA陣列的一部分。這個轉化過程我們通常使用flex來完成。我們只要定義號一個語言的正則表示式,其他的NFA,DFA就直接交給flex來解決就可以了。但是,知道原理不是更讓人自由麼?

     以下是生成tiger語言的flex正則表示式(因為編碼(lan)的原因,所以,在輸出常量字串的時候是一個字母一個字母的輸出的,很好改,可以自己動手改一下):

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 %{ #include <string.h> #include "util.h" #include "tokens.h" #include "errormsg.h" int  charPos=1; int  count =  0 ;   #ifdef __cplusplus extern  "C"  int  yywrap ( void  ) #else extern  int  yywrap ( void  ) #endif {      charPos = 1 ;      return  1 ; }   void  adjust( void ) {   EM_tokPos=charPos;   charPos+=yyleng; }   %}     %state  COMMENT %state  CONST_STRING   %% <INITIAL>[\"]   {adjust(); yylval.sval = string(yytext) ; BEGIN CONST_STRING;  return  CONSTSTR;} <INITIAL> " "    {adjust();   continue ;} <INITIAL>\n    {adjust(); EM_newline();  continue ;} <INITIAL>\t  {adjust();  continue ;} <INITIAL> ","    {adjust();  return  COMMA;} <INITIAL>:    {adjust();  return  COLON;} <INITIAL>; {adjust();  return  SEMICOLON;} <INITIAL> "("   {adjust();  return  LPAREN;} <INITIAL> ")"   {adjust();  return  RPAREN;} <INITIAL> "["   {adjust();  return  LBRACK;} <INITIAL> "]"   {adjust();  return  RBRACK;} <INITIAL> "{"    {adjust();  return  LBRACE;} <INITIAL> "}"    {adjust();  return  RBRACE;} <INITIAL> "."    {adjust();  return  DOT;} <INITIAL> "+"    {adjust();  return  PLUS;} <INITIAL> "-"    {adjust();  return  MINUS;} <INITIAL> "*"    {adjust();  return  TIMES;} <INITIAL> "/"    {adjust();  return  DIVIDE;} <INITIAL> "="    {adjust();  return  EQ;} <INITIAL> "<>"   {adjust();  return  NEQ;} <INITIAL> "<"    {adjust();  return  LT;} <INITIAL> "<="   {adjust();  return  LE;} <INITIAL> ">"    {adjust();  return  GT;} <INITIAL> ">="   {adjust();  return  GE;} <INITIAL> "&"    {adjust();  return  AND;} <INITIAL> "|"    {adjust();  return  OR;} <INITIAL>:=  {adjust();  return  ASSIGN;} <INITIAL> for        {adjust();  return  FOR;} <INITIAL>array {adjust();  return  ARRAY;} <INITIAL>string   {adjust();  return  STRING;} <INITIAL> if    {adjust();  return  IF;} <INITIAL>then {adjust();  return  THEN;} <INITIAL> else  {adjust();  return  ELSE;}  <INITIAL> while  {adjust();  return  WHILE;} <INITIAL>to   {adjust();  return  TO;} <INITIAL> do    {adjust();  return  DO;} <INITIAL>let  {adjust();  return  LET;} <INITIAL>in  {adjust();  return  IN;} <INITIAL>end  {adjust();  return  END;} <INITIAL>of  {adjust();  return  OF;} <INITIAL> break  {adjust();  return  BREAK;} <INITIAL>nil  {adjust();  return  NIL;} <INITIAL>function {adjust();  return  FUNCTION;} <INITIAL>var {adjust();  return  VAR;} <INITIAL>type {adjust();  return  TYPE;} <INITIAL> int   {adjust();  return  INT  ;} <INITIAL>[0-9]+    {adjust(); yylval.ival= atoi (yytext);  return  NUM;} <INITIAL>([0-9]+ "." [0-9]*)|([0-9]* "." [0-9]+) {adjust(); yylval.fval= atoi (yytext);  return  REAL;} <INITIAL>[a-zA-Z][a-zA-Z0-9]*   {adjust();yylval.sval=string(yytext); return  ID;} <INITIAL> "/*"   {adjust();count++; BEGIN COMMENT;} <CONST_STRING>[\"]  {adjust(); yylval.sval = string(yytext); BEGIN INITIAL ;  return  CONSTSTR;} <CONST_STRING> " "  {adjust(); yylval.sval = string(yytext) ;   return  CONSTSTR;} <CONST_STRING>. {adjust(); yylval.sval = string(yytext);  return  CONSTSTR; } <COMMENT> "*/"    {adjust();count--;  if (count == 0) BEGIN INITIAL;} <COMMENT>. {adjust(); } <COMMENT>\n {adjust();  }

    相對於虎書上的定義的一些型別,我又多定義了三個,分別是NUM,CONSTSTR,REAL。所以,必須要修改相應的程式碼,新增巨集定義,以及現實字符集。

   在實現過程中,遇到一些問題,記錄下來:

   1.如果在使用包含有c的c++程式碼時候 要使用
  #ifdef __cplusplus
  static int yyinput (void );
  #else
  static int input (void );
  #endif

  #ifdef __cplusplus
  extern "C" int yywrap (void )
  #else
  extern int yywrap (void )
  #endif
  {
  charPos = 1 ;
  return 1 ;
  }

    使用以上的方式經行定義,否則將會出現 函式定義不規範的報錯。

   2.在vs中使用#include <io.h>  ,#include <process.h> 來代替 #include <unistd.h> 後者是在linux中使用的規範

  3.還有可能出現 warning:“rule cannot be matched”。這表示你所定義的一個正則表示式並沒有用 ,可能在前面已經定過了,或者使用規則優先原則 ,這個表示式可能不會使用。