Lex和Yacc應用方法(二).再識Lex與Yacc

來源:互聯網
上載者:User

Lex和Yacc應用方法(二).再識Lex與Yacc

草木瓜  20070314

早在二十世記七十年代之前,編寫編譯器一直是一個非常費時的工作。但到了1975這
一年這一切卻發生了重大轉變,首先Stephen C. Johnson Lesk在貝爾實驗室完成了
Yacc開發,為了配合yacc更好的協作, Mike Lesk和Eric Schmidt又完成了lex。從
而Lex和yacc成為電腦編譯領域的重要理論,而這些工具也極大地簡化了編寫編譯
器的工作。

後來Robert Corbett和Richard Stallman在此基礎上又完成了bison。Jef Poskanzer,
Vern Paxson 也對lex作了大量改進,稱為flex。

<本系列文章的地址:http://blog.csdn.net/liwei_cmg/category/207528.aspx>

一、Lex理論

Lex使用Regex從輸入代碼中掃描和匹配字串。每一個字串會對應一個動作。
通常動作返回一個標記給後面的剖析器使用,代表被匹配的字串。每一個正則表達
式代表一個有限狀態自動機(FSA)。我們可以用狀態和狀態間的轉換來代表一個(FSA)。
其中包括一個開始狀態以及一個或多個結束狀態或接受狀態。

我們以上文《Lex和Yacc應用方法(一).初識Lex》第一個例子詳細說明:

exfirst.l

%{
#include "stdio.h"
%}
%%
[/n]                  ;                                    A
[0-9]+                printf("Int     : %s/n",yytext);     B
[0-9]*/.[0-9]+        printf("Float   : %s/n",yytext);     C
[a-zA-Z][a-zA-Z0-9]*  printf("Var     : %s/n",yytext);     D
[/+/-/*///%]          printf("Op      : %s/n",yytext);     E
.                     printf("Unknown : %c/n",yytext[0]);  F
%%

這裡使用一相對簡單的輸入檔案 file.txt

i=1.344+39;
bcd=4%9-333

我們假定,
Lex 系統建立一動態列表:內容+規則+狀態
Lex 狀態:1 接受 2 結束

接受表示該元素可以做為模式比對
結束表示該元素已完成模式比對

讀入“i” 
   [尋找元素]尋找相鄰且狀態為1的元素,無元素,
       [匹配規則]D,
       [新增列表<元素1>共置資料](存在則覆蓋)狀態為1,規則為D,內容為"i"。
       [操作順序符] 1
讀入“=” 
   [尋找元素]尋找相鄰且狀態為1的元素,“i=”尋找匹配規則,無規則
       [置上一元素]<元素1>狀態為2
       [匹配規則]F,
       [新增列表<元素2>共置資料](存在則覆蓋)狀態為1,規則為F,內容為"="
       [操作順序符] 2
讀入“1”,
   [尋找元素]尋找相鄰且狀態為1的元素,“=1”尋找匹配規則,無規則
       [置上一元素]<元素2>狀態為2
       [匹配規則]B,
       [新增列表<元素3>共置資料](存在則覆蓋)狀態為1,規則為B,內容為"1"
       [操作順序符] 3
讀入“.” 
   [尋找元素]尋找相鄰且狀態為1的元素,“1.”尋找匹配規則,無規則,但有潛在規則C
       [匹配規則]F,
       [新增列表<元素4>共置資料](存在則覆蓋)狀態為1,規則為F,內容為"."
       [置上一元素]<元素3>狀態為1
       [操作順序符] 4
讀入“3” 
   [尋找元素]尋找相鄰且狀態為1的元素,“1.3”尋找匹配規則,有規則       
       [置起始元素]狀態為1,規則為C,內容為"1.3"
       [操作順序符] 3 組合元素的起始操作符
讀入“4”         
   [尋找元素]尋找相鄰且狀態為1的元素,“1.34”尋找匹配規則,有規則
       [置起始元素]狀態為1,規則為C,內容為"1.34"
       [操作順序符] 3 組合元素的起始操作符
讀入“4”         
   [尋找元素]尋找相鄰且狀態為1的元素,“1.344”尋找匹配規則,有規則
       [置起始元素]狀態為1,規則為C,內容為"1.344"
       [操作順序符] 3 組合元素的起始操作符
讀入“+”         
   [尋找元素]尋找相鄰且狀態為1的元素,“1.344+”尋找匹配規則,無規則
      [匹配規則]E,
      [新增列表<元素4>共置資料](存在則覆蓋)狀態為1,規則為E,內容為"+"
      [置上一元素]<元素3>狀態為2
      [操作順序符] 4

...

最後解析結果為
           內容    規則    狀態
<元素1>    i       D       2
<元素2>    =       F       2
<元素3>    1.344   C       2
<元素4>    +       E       2
...

上面列出的演算法是僅屬於個人的分析,是相對直觀且便於理解的,也可以參照這個演算法
用C語言類比出lex的效果。不過真正的Lex演算法肯定是更為複雜的理論體系,這個沒有
接觸過,有興趣可以參看相關資料。

二、yacc的BNF檔案

    個人認為lex理論比較容易理解的,yacc要複雜一些。
    我們先從yacc的文法說起。yacc文法採用BNF(Backus-Naur Form)的變數規則描
述。BNF文法最初由John Backus和Peter Naur發明,並且用於描述Algol60語言。BNF
能夠用於表達上下文無關的語言。現代程式語言大多數結構能夠用BNF來描述。
   
    舉個加減乘除例子來說明:
   
    1+2/3+4*6-3
   
    BNF文法:
                          優先順序
                         
    E = num      規約a    0
    E = E / E    規約b    1
    E = E * E    規約c    1
    E = E + E    規約d    2
    E = E - E    規約e    2
   
    這裡像(E運算式)這樣出現在左邊的結構叫做非終結符(nonterminal)。像(num
標識符)這樣的結構叫終結符(terminal,讀了後面內容就會發現,其實是由lex返回
的標記),它們只出現在右邊。
   

    我們將 “1+2/3+4*6-3-2”逐個字元移進堆棧,如下所示:
   
           .1+2/3+4*6-3    
    1      1.+2/3+4*6-3     移進
    2      E.+2/3+4*6-3     規約a
    3      E+.2/3+4*6-3     移進
    4      E+2./3+4*6-3     移進
    5      E+E./3+4*6-3     規約a
    6      E+E/.3+4*6-3     移進
    7      E+E/3.+4*6-3     移進
    8      E+E/E.+4*6-3     規約a
    9      E+E/E+.4*6-3     移進
    10     E+E/E+4.*6-3     移進
    11     E+E/E+E.*6-3     規約a
    12     E+E/E+E*.6-3     移進
    13     E+E/E+E*6.-3     移進
    14     E+E/E+E*E.-3     規約a
    15     E+E/E+E*E-.3     移進
    16     E+E/E+E*E-3.     移進
    17     E+E/E+E*E-E.     規約a
   
    18     E+E+E*E-E.       規約b
    19     E+E+E-E.         規約c
    20     E+E-E.           規約d
    21     E-E.             規約d
    22     E.               規約e
   
    我們在實際運算操作中是把一個運算式逐步簡化成一個非終結符。稱之為“自底
向上”或者“移進歸約”的分析法。
    點左面的結構在堆棧中,而點右面的是剩餘的輸入資訊。我們以把標記移入堆棧開
始。當堆棧頂部和右式要求的記號匹配時,我們就用左式取代所匹配的標記。概念上,
匹配右式的標記被彈出堆棧,而左式被壓入堆棧。我們把所匹配的標記認為是一個控制代碼,
而我們所做的就是把控制代碼向左式歸約。這個過程一直持續到把所有輸入都壓入堆棧中,
而最終堆棧中只剩下最初的非終結符。
    在第1步中我們把1壓入堆棧中。第2步對應規則a,把1轉換成E。然後繼續壓入和歸
約,直到第5步。此時堆棧中剩下E+E,按照規則d,可以進行E=E+E的合并,然而輸入信
息並沒有結束,這就產生了“移進-歸約”衝突(shift-reduce conflict)。在yacc中產
生這種衝突時,會繼續移進。
    在第17步,E+E/E,即可以採用E+E規則d,也可以採用E/E規則b,如果使用E=E+E
規約,顯然從演算法角度是錯誤的,這就有了運算子的優先順序概念。這種情況稱為“歸約
-歸約”衝突(reduce-reduce conflict)。此時yacc會採用第一條規則,即E=E/E。這
個內容會在後面的執行個體做進一步深化。

三、十分典型的利用lex和yacc類比的簡單+-*/計算機。

    A.樣本

  最有效方法是樣本學習,這樣首先給出全部樣本檔案。
  
  lex檔案:lexya_a.l
  
  %{
  #include <stdlib.h>
  void yyerror(char *);
  #include "lexya_a.tab.h"
  %}
  %%
  [0-9]+       { yylval = atoi(yytext); return INTEGER; }
  [-+*//n]     return *yytext;
  [/t]         ;/* 去除空格 */
  .            yyerror("無效字元");
  %%
  int yywrap(void) {
  return 1;
  }
  
  yacc檔案:lexya_a.y
  
  %{
  #include <stdlib.h>
  int yylex(void);
  void yyerror(char *);
  %}
  %token INTEGER
  %left '+' '-'
  %left '*' '/'
  %%
  program:
  program expr '/n' { printf("%d/n", $2); }
  |
  ;
  expr:
  INTEGER { $$ = $1; }
  | expr '*' expr { $$ = $1 * $3; }
  | expr '/' expr { $$ = $1 / $3; }
  | expr '+' expr { $$ = $1 + $3; }
  | expr '-' expr { $$ = $1 - $3; }
  ;
  %%
  void yyerror(char *s) {
  printf("%s/n", s);
  }
  int main(void) {
  yyparse();
  return 0;
  }
  
  進行編譯:
  bison -d lexya_a.y
  lex lexya_a.l
  cc -o parser *.c
  
  運行:
  ./parser
  輸入計算式,斷行符號會顯示運算結果
  
  如:
  
  1+2*5+10/5
  13
  9+8/3
  11
  10+2-2/2-2*5
  1
  
  
  這裡有兩個檔案lexya_a.y和lexya_a.l。lexya_a.y是yacc檔案,bison -d lexya_a.y
編譯後會產生 lexya_a.tab.c  lexya_a.tab.h。lex檔案lexya_a.l中頭聲明已包括了
lexya_a.tab.h。這兩個檔案是典型的互相協作的樣本。
   
   
    B.分析
   
   
    (1)定義段和預定義標記部分
   
    yacc檔案定義與lex十分相似,分別以%{ }% %% %%分界。
   
  %{
  #include <stdlib.h>
  int yylex(void);
  void yyerror(char *);
  %}
  
  這一段十分容易理解,只是標頭檔一些引用說明。稱為“定義”段。  
  
  %}
  %token INTEGER
  %left '+' '-'
  %left '*' '/'
  %%
  
  %}和%%這一段可以看作預定義標記部分。%token INTEGER 定義聲明了一個標記。
當我們編譯後,它會在lexya_a.tab.c中產生一個剖析器,同時會在lexya_a.tab.h
產生包含資訊:
    # define INTEGER 257
    其中0-255的之間的標記值約定為字元值,是系統保留的後定義的token。
   
    lexya_a.tab.h其它部分是預設產生的,與token INTEGER無關。
    # ifndef YYSTYPE
    #  define YYSTYPE int
    #  define YYSTYPE_IS_TRIVIAL 1
    # endif
   
    extern YYSTYPE yylval;
   
    lex檔案需要包含這個標頭檔,並且使用其中對標記值的定義。為了獲得標記,yacc
會調用yylex。yylex的傳回值類型是整型,可以用於返回標記。而在yylval變數中保
存著與返回的標記相對應的值。

    yacc在內部維護著兩個堆棧,一個分析棧和一個內容棧。分析棧中儲存著終結符和
非終結符,並且記錄了當前剖析狀態。而內容棧是一個YYSTYPE類型的元素數組,對於分
析棧中的每一個元素都儲存著一個對應的值。例如,當yylex返回一個INTEGER標記時,
把這個標記移入分析棧。同時,相應的yacc值將會被移入內容棧中。分析棧和內容棧的
內容總是同步的,因此從棧中找到對應的標記值是很容易的。
   
    比如lex檔案中下面這一段:
  [0-9]+       { yylval = atoi(yytext); return INTEGER; }
  
    這是將把整數的值儲存在yylval中,同時向yacc返回標記INTEGER。即內容棧存在
了整數的值,對應的分析棧就為INTEGER標記了。yylval類型由YYSTYPE決定,由於它的
預設類型是整型,所以在這個例子中程式運行正常。

    lex檔案還有一段:
    [-+*//n]     return *yytext;
    這裡顯然只是向yacc的分析棧返回運算子標記,系統保留的0-255此時便有了作用,
內容棧為空白。把“-”放在第一位是防止Regex發現類似a-z的歧義。
   
    再看下面的:
   
    %left '+' '-'
    %left '*' '/'

    %left 表示左結合,%right 表示右結合。最後列出的定義擁有最高的優先權。因
此乘法和除法擁有比加法和減法更高的優先權。+ - * / 所有這四個算術符都是左結合
的。運用這個簡單的技術,我們可以消除文法的歧義。

    註:關於結合性,各運算子的結合性分為兩種,即左結合性(自左至右)和右結合性
(自右至左)。例如算術運算子的結合性是自左至右,即先左後右。如有運算式x-y+z則y
應先與“-”號結合, 執行x-y運算,然後再執行+z的運算。這種自左至右的結合方向
就稱為“左結合性”。而自右至左的結合方向稱為“右結合性”。 最典型的右結合性運
算符是賦值運算子。如x=y=z,由於“=”的右結合性,應先執行y=z再執行x=(y=z)運算。
   
    (2)規則部分
   
  %%
  program:
  program expr '/n' { printf("%d/n", $2); }
  |
  ;
    expr:
  INTEGER { $$ = $1; }
  | expr '*' expr { $$ = $1 * $3; }
  | expr '/' expr { $$ = $1 / $3; }
  | expr '+' expr { $$ = $1 + $3; }
  | expr '-' expr { $$ = $1 - $3; }
  ;
  %%
  
  這個規則乍看起來的確有點暈,關鍵一點就是要理解yacc的遞迴解析方式。
  
  program和expr是規則標記,但是作為一個整體描述運算式。
  
  先看expr,可以由單個INTEGER值組成,也可以有多個INTERGER和運算子組合組
成。
    以運算式“1+4/2*3-0”為例,1 4 2 3 都是expr,就是expr+expr/expr*expr-expr
說到底最後還是個expr。遞迴思想正好與之相反,逆推下去會發現expr這個規則標記
能表示所有的數值運算運算式。

    瞭解了expr後,再看program,首先program可以為空白,也可以用單單的expr加下
“/n”斷行符號符組成,結合起來看program定義的就是多個運算式組成的檔案內容。
   
    回過頭,建立如下檔案input:
   
  [root@localhost yacc]# cat input
  1+5/5+4*5
  3+9+2*10-9
  2/2
  3-9
  
  運行則結果如下:
  [root@localhost yacc]# ./parser < input
  22
  23
  1
  -6
  
  粗略有了概念之後,再看lex如何執行相應的行為。
  
    以 expr: expr '+' expr { $$ = $1 + $3; }為例:
    在分析棧中我們其實用左式替代了右式。在本例中,我們彈出“ expr '+' expr ”
然後壓入“expr”。我們通過彈出三個成員,壓入一個成員來縮小堆棧。在我們的代碼中
可以看到用相對位址訪問內容棧中的值。如$1,$2,這樣都是yacc預定義可以直接使用的
標記。“$1”代表右式中的第一個成員,“$2”代表第二個,後面的以此類推。“$$”
表示縮小後的堆棧頂部。在上面的動作中,把對應兩個運算式的值相加,彈出內容棧中的三
個成員,然後把得到的和壓入堆棧中。這樣,保持分析棧和內容棧中的內容依然同步。

    而
    program:
    program expr '/n' { printf("%d/n", $2); }
    說明每當一行運算式結束時,列印出第二個棧值,即expr的值,完成字元運算。
   
   

四.後記

   
    如果到這裡能完全理解所述內容,對於lex和yacc就有些感覺了。後面文章會做進一步
的深入。 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.