一個Lex/Yacc完整的示例(轉)

本框架是一個lex/yacc完整的示例,包括詳細的註釋,用於學習lex/yacc程序基本的搭建方法,在linux/cygwin下敲入make就可以編譯和執行。

大部分框架已經搭好了,你只要稍加擴展就可以成爲一個計算器之類的程序,用於《編譯原理》的課程設計,或者對照理解其它lex/yacc項目的代碼。

本例子雖小卻演示了lex/yacc程序最重要和常用的特徵:

* lex/yacc程序組成結構、文件格式。  
* 如何在lex/yacc中使用C++和STL庫,用extern "C"聲明那些lex/yacc生成的、要鏈接的C函數,如yylex(), yywrap(), yyerror()。  
* 重定義YYSTYPE/yylval爲複雜類型。  
* lex裏多狀態的定義和使用,用BEGIN宏在初始態和其它狀態間切換。  
* lex里正則表達式的定義、識別方式。  
* lex裏用yylval向yacc返回數據。  
* yacc裏用%token<>方式聲明yacc記號。  
* yacc裏用%type<>方式聲明非終結符的類型。  
* 在yacc嵌入的C代碼動作裏,對記號屬性($1, $2等)、和非終結符屬性($$)的正確引用方法。  
* 對yyin/yyout重賦值,以改變yacc默認的輸入/輸出目標。  

本例子功能是,對當前目錄下的file.txt文件,解析出其中的標識符、數字、其它符號,顯示在屏幕上。linux調試環境是Ubuntu 10.04。

文件列表:

lex.l:      lex程序文件。  
yacc.y:     yacc程序文件。  
main.h:     lex.l和yacc.y共同使用的頭文件。  
Makefile:       makefile文件。  
lex.yy.c:       用lex編譯lex.l後生成的C文件。  
yacc.tab.c: 用yacc編譯yacc.y後生成的C文件。  
yacc.tab.h: 用yacc編譯yacc.y後生成的C頭文件,內含%token、YYSTYPE、yylval等定義,供lex.yy.c和yacc.tab.c使用。  
file.txt:       被解析的文本示例。  
README.txt: 本說明。  

下面列出主要的代碼文件:

main.h: lex.l和yacc.y共同使用的頭文件

#ifndef MAIN_HPP  
#define MAIN_HPP  

#include <iostream>//使用C++庫  
#include <string>  
#include <stdio.h>//printf和FILE要用的  

using namespace std;  

/*當lex每識別出一個記號後,是通過變量yylval向yacc傳遞數據的。默認情況下yylval是int類型,也就是隻能傳遞整型數據。 
yylval是用YYSTYPE宏定義的,只要重定義YYSTYPE宏,就能重新指定yylval的類型(可參見yacc自動生成的頭文件yacc.tab.h)。 
在我們的例子裏,當識別出標識符後要向yacc傳遞這個標識符串,yylval定義成整型不太方便(要先強制轉換成整型,yacc裏再轉換回char*)。 
這裏把YYSTYPE重定義爲struct Type,可存放多種信息*/  
struct Type//通常這裏面每個成員,每次只會使用其中一個,一般是定義成union以節省空間(但這裏用了string等複雜類型造成不可以)  
{  
    string m_sId;  
    int m_nInt;  
    char m_cOp;  
};  

#define YYSTYPE Type//把YYSTYPE(即yylval變量)重定義爲struct Type類型,這樣lex就能向yacc返回更多的數據了  

#endif  

lex.l: lex程序文件

%{  
/*本lex的生成文件是lex.yy.c 
lex文件由3段組成,用2個%%行把這3段隔開。 
第1段是聲明段,包括: 
1-C代碼部分:include頭文件、函數、類型等聲明,這些聲明會原樣拷到生成的.c文件中。 
2-狀態聲明,如%x COMMENT。 
3-正則式定義,如digit ([0-9])。 
第2段是規則段,是lex文件的主體,包括每個規則(如identifier)是如何匹配的,以及匹配後要執行的C代碼動作。 
第3段是C函數定義段,如yywrap()的定義,這些C代碼會原樣拷到生成的.c文件中。該段內容可以爲空*/  

//第1段:聲明段  
#include "main.h"//lex和yacc要共用的頭文件,裏面包含了一些頭文件,重定義了YYSTYPE  
#include "yacc.tab.h"//用yacc編譯yacc.y後生成的C頭文件,內含%token、YYSTYPE、yylval等定義(都是C宏),供lex.yy.c和yacc.tab.c使用  

extern "C"//爲了能夠在C++程序裏面調用C函數,必須把每一個需要使用的C函數,其聲明都包括在extern "C"{}塊裏面,這樣C++鏈接時才能成功鏈接它們。extern "C"用來在C++環境下設置C鏈接類型。  
{   //yacc.y中也有類似的這段extern "C",可以把它們合併成一段,放到共同的頭文件main.h中  
    int yywrap(void);  
    int yylex(void);//這個是lex生成的詞法分析函數,yacc的yyparse()裏會調用它,如果這裏不聲明,生成的yacc.tab.c在編譯時會找不到該函數  
}  
%}  

/*lex的每個正則式前面可以帶有"<狀態>",例如下面的"<COMMENT>\n"。每個狀態要先用%x聲明才能使用。 
當lex開始運行時,默認狀態是INITIAL,以後可在C代碼裏用"BEGIN 狀態名;"切換到其它狀態(BEGIN是lex/yacc內置的宏)。 
這時,只有當lex狀態切換到COMMENT後,纔會去匹配以<COMMENT>開頭的正則式,而不匹配其它狀態開頭的。 
也就是說,lex當前處在什麼狀態,就考慮以該狀態開頭的正則式,而忽略其它的正則式。 
其應用例如,在一段C代碼裏,同樣是串"abc",如果它寫在代碼段裏,會被識別爲標識符,如果寫在註釋裏則就不會。所以對串"abc"的識別結果,應根據不同的狀態加以區分。 
本例子需要忽略掉文本中的行末註釋,行末註釋的定義是:從某個"//"開始,直到行尾的內容都是註釋。其實現方法是: 
1-lex啓動時默認是INITIAL狀態,在這個狀態下,串"abc"會識別爲標識符,串"123"會識別爲整數等。 
2-一旦識別到"//",則用BEGIN宏切換到COMMENT狀態,在該狀態下,abc這樣的串、以及其它字符會被忽略。只有識別到換行符\n時,再用BEGIN宏切換到初始態,繼續識別其它記號。*/  
%x COMMENT  

/*非數字由大小寫字母、下劃線組成*/  
nondigit    ([_A-Za-z])  

/*一位數字,可以是0到9*/  
digit       ([0-9])  

/*整數由1至多位數字組成*/  
integer     ({digit}+)  

/*標識符,以非數字開頭,後跟0至多個數字或非數字*/  
identifier  ({nondigit}({nondigit}|{digit})*)  

/*一個或一段連續的空白符*/  
blank_chars ([ \f\r\t\v]+)  

/*下面%%後開始第2段:規則段*/  
%%  

{identifier}    {   //匹配標識符串,此時串值由yytext保存  
            yylval.m_sId=yytext;//通過yylval向yacc傳遞識別出的記號的值,由於yylval已定義爲
                                //struct Type,這裏就可以把yytext賦給其m_sId成員,
                                //到了yacc裏就可以用$n的方式來引用了  

            return IDENTIFIER;  //向yacc返回: 識別出的記號類型是IDENTIFIER  
        }  

{integer}       {   //匹配整數串  
            yylval.m_nInt=atoi(yytext);//把識別出的整數串,轉換爲整型值,
                                       //存儲到yylval的整型成員裏,到了yacc裏用$n方式引用  
            return INTEGER;//向yacc返回: 識別出的記號類型是INTEGER  
        }  

{blank_chars}   {   //遇空白符時,什麼也不做,忽略它們  
        }  

\n      {   //遇換行符時,忽略之  
        }  
"//"        {   //遇到串"//",表明要開始一段註釋,直到行尾  
            cout<<"(comment)"<<endl;//提示遇到了註釋  
            BEGIN COMMENT;//用BEGIN宏切換到註釋狀態,去過濾這段註釋,
                          //下一次lex將只匹配前面帶有<COMMENT>的正則式  
        }  

.       {   //.表示除\n以外的其它字符,注意這個規則要放在最後,
            // 因爲一旦匹配了.就不會匹配後面的規則了(以其它狀態<>開頭的規則除外)  
            yylval.m_cOp=yytext[0]; //由於只匹配一個字符,這時它對應yytext[0],
                                    //該字符存放到yylval的m_cOp成員裏,到了yacc裏用$n方式引用  
            return OPERATOR;//向yacc返回: 識別出的記號類型是OPERATOR  
        }  

<COMMENT>\n   {   //註釋狀態下的規則,只有當前切換到COMMENT狀態纔會去匹配  
            BEGIN INITIAL;//在註釋狀態下,當遇到換行符時,表明註釋結束了,返回初始態  
        }  

<COMMENT>.    {   //在註釋狀態下,對其它字符都忽略,即:註釋在lex(詞法分析層)就過濾掉了,      
                  //不返回給yacc了  
        }  

%%  

//第3段:C函數定義段  
int yywrap(void)  
{  
    puts("-----the file is end");  
    return 1;//返回1表示讀取全部結束。如果要接着讀其它文件,可以這裏fopen該文件,
             //文件指針賦給yyin,並返回0  
}  

yacc.y: yacc程序文件

%{  
/*本yacc的生成文件是yacc.tab.c和yacc.tab.h 
yacc文件由3段組成,用2個%%行把這3段隔開。 
第1段是聲明段,包括: 
1-C代碼部分:include頭文件、函數、類型等聲明,這些聲明會原樣拷到生成的.c文件中。 
2-記號聲明,如%token 
3-類型聲明,如%type 
第2段是規則段,是yacc文件的主體,包括每個產生式是如何匹配的,以及匹配後要執行的C代碼動作。 
第3段是C函數定義段,如yyerror()的定義,這些C代碼會原樣拷到生成的.c文件中。該段內容可以爲空*/  

//第1段:聲明段  
#include "main.h"//lex和yacc要共用的頭文件,裏面包含了一些頭文件,重定義了YYSTYPE  

extern "C"//爲了能夠在C++程序裏面調用C函數,必須把每一個需要使用的C函數,其聲明都包括在extern "C"{}      
          //塊裏面,這樣C++鏈接時才能成功鏈接它們。extern "C"用來在C++環境下設置C鏈接類型。  
{   //lex.l中也有類似的這段extern "C",可以把它們合併成一段,放到共同的頭文件main.h中  
    void yyerror(const char *s);  
    extern int yylex(void);//該函數是在lex.yy.c裏定義的,yyparse()裏要調用該函數,
                            //爲了能編譯和鏈接,必須用extern加以聲明  
}  

%}  

/*lex裏要return的記號的聲明 
用token後加一對<member>來定義記號,旨在用於簡化書寫方式。 
假定某個產生式中第1個終結符是記號OPERATOR,則引用OPERATOR屬性的方式: 
1-如果記號OPERATOR是以普通方式定義的,如%token OPERATOR,則在動作中要寫$1.m_cOp,以指明使用YYSTYPE的哪個成員 
2-用%token<m_cOp>OPERATOR方式定義後,只需要寫$1,yacc會自動替換爲$1.m_cOp 
另外用<>定義記號後,非終結符如file, tokenlist,必須用%type<member>來定義(否則會報錯),以指明它們的屬性對應YYSTYPE中哪個成員,這時對該非終結符的引用,如
,會自動替換爲
.member*/  
%token<m_nInt>INTEGER  
%token<m_sId>IDENTIFIER  
%token<m_cOp>OPERATOR  
%type<m_sId>file  
%type<m_sId>tokenlist  

%%  

file:   //文件,由記號流組成  
    tokenlist   //這裏僅顯示記號流中的ID  
    {  
        cout<<"all id:"<<$1<<endl;    //$1是非終結符tokenlist的屬性,由於該終結符是
        //用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$1相當於$1.m_sId,
        //其值已經在下層產生式中賦值(tokenlist IDENTIFIER)  
    };  
tokenlist://記號流,或者爲空,或者由若干數字、標識符、及其它符號組成  
    {  
    }  
    | tokenlist INTEGER  
    {  
        cout<<"int: "<<$2<<endl;//$2是記號INTEGER的屬性,由於該記號是用%token<m_nInt>定義的,
                //即約定對其用YYSTYPE的m_nInt屬性,$2會被替換爲yylval.m_nInt,已在lex裏賦值  
    }  
    | tokenlist IDENTIFIER  
    {  
        $$+=" " + $2;//是非終結符tokenlist的屬性,由於該終結符是用
        //相當於$$.m_sId,這裏把識別到的標識符串保存在tokenlist屬性中,到上層產生式裏可以拿出爲用  

        cout<<"id: "<<$2<<endl;//$2是記號IDENTIFIER的屬性,由於該記號是用%token<m_sId>定義的,
                     //即約定對其用YYSTYPE的m_sId屬性,$2會被替換爲yylval.m_sId,已在lex裏賦值  
    }  
    | tokenlist OPERATOR  
    {  
        cout<<"op: "<<$2<<endl;//$2是記號OPERATOR的屬性,由於該記號是用%token<m_cOp>定義的,
                      //即約定對其用YYSTYPE的m_cOp屬性,$2會被替換爲yylval.m_cOp,已在lex裏賦值  
    };  

%%  

void yyerror(const char *s) //當yacc遇到語法錯誤時,會回調yyerror函數,並且把錯誤信息放在參數s中  
{  
    cerr<<s<<endl;//直接輸出錯誤信息  
}  

int main()//程序主函數,這個函數也可以放到其它.c, .cpp文件裏  
{  
    const char* sFile="file.txt";//打開要讀取的文本文件  
    FILE* fp=fopen(sFile, "r");  
    if(fp==NULL)  
    {  
        printf("cannot open %s\n", sFile);  
        return -1;  
    }  
    extern FILE* yyin;  //yyin和yyout都是FILE*類型  
    yyin=fp;//yacc會從yyin讀取輸入,yyin默認是標準輸入,
            //這裏改爲磁盤文件。yacc默認向yyout輸出,可修改yyout改變輸出目的  

    printf("-----begin parsing %s\n", sFile);  
    yyparse();//使yacc開始讀取輸入和解析,它會調用lex的yylex()讀取記號  
    puts("-----end parsing");  

    fclose(fp);  

    return 0;  
}  

Makefile: makefile文件

LEX=flex  
YACC=bison  
CC=g++  
OBJECT=main #生成的目標文件  

$(OBJECT): lex.yy.o  yacc.tab.o  
    $(CC) lex.yy.o yacc.tab.o -o $(OBJECT)  
    @./$(OBJECT) #編譯後立刻運行  

lex.yy.o: lex.yy.c  yacc.tab.h  main.h  
    $(CC) -c lex.yy.c  

yacc.tab.o: yacc.tab.c  main.h  
    $(CC) -c yacc.tab.c  

yacc.tab.c  yacc.tab.h: yacc.y  
# bison使用-d參數編譯.y文件  
    $(YACC) -d yacc.y  

lex.yy.c: lex.l  
    $(LEX) lex.l  

clean:  
    @rm -f $(OBJECT)  *.o  

file.txt: 被解析的文本示例

abc defghi  
//this line is comment, abc 123 !@#$  
123 45678   //comment until line end  
!   @   #   $  

使用方法:
1-把lex_yacc_example.rar解壓到linux/cygwin下。
2-命令行進入lex_yacc_example目錄。
3-敲入make,這時會自動執行以下操作:
(1) 自動調用flex編譯.l文件,生成lex.yy.c文件。
(2) 自動調用bison編譯.y文件,生成yacc.tab.c和yacc.tab.h文件。
(3) 自動調用g++編譯、鏈接出可執行文件main。
(4) 自動執行main。
運行結果如下所示:

bison -d yacc.y  
g++ -c lex.yy.c  
g++ -c yacc.tab.c  
g++ lex.yy.o yacc.tab.o -o main           
-----begin parsing file.txt  
id: abc  
id: defghi  
(comment)  
int: 123  
int: 45678  
(comment)  
op: !  
op: @  
op: #  
op: $  
-----the file is end  
all id: abc defghi  
-----end parsing  

參考鏈接:
http://blog.csdn.net/huyansoft/article/details/8860224

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