通過實例深入理解lec和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的屬性,由於該終結符是用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$$相當於$$.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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM