1. 程式人生 > 實用技巧 >lex和yacc學習

lex和yacc學習

main.h檔案

#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,可存放多種資訊
 */

/* 通常這裡面每個成員,每次只會使用其中一個,一般是定義成union以節省空間(但這裡用了string等複雜型別造成不可以) */
struct Type {
    string m_sId;
    int m_nInt;
    char m_cOp;
};

/* 把YYSTYPE(即yylval變數)重定義為struct Type型別,這樣lex就能向yacc返回更多的資料了 */
#define YYSTYPE Type

#endif

lex.l檔案

%{

/*
 * 本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使用 */

/* 
 * 為了能夠在C++程式裡面呼叫C函式,必須把每一個需要使用的C函式,其宣告都包括在extern "C"{}塊裡面,
 * 這樣C++連結時才能成功連結它們。extern "C"用來在C++環境下設定C連結型別。
 */
extern "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])   /* 非數字由大小寫字母、下劃線組成 */
digit       ([0-9])       /* 一位數字,可以是0到9 */
integer     ({digit}+)    /* 整數由1至多位數字組成 */
identifier  ({nondigit}({nondigit}|{digit})*)  /* 識別符號,以非數字開頭,後跟0至多個數字或非數字 */
blank_chars ([ \f\r\t\v]+)  				   /* 一個或一段連續的空白符 */

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

%%

// 匹配識別符號串,此時串值由yytext儲存
{identifier} {
	
	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狀態才會去匹配
<COMMENT>\n {    
    BEGIN INITIAL;	// 在註釋狀態下,當遇到換行符時,表明註釋結束了,返回初始態
}

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

%%

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

yacc.y檔案

%{

/*
 * 本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

/*
 * 為了能夠在C++程式裡面呼叫C函式,必須把每一個需要使用的C函式,其宣告都包括在extern "C"{}塊裡面,
 * 這樣C++連結時才能成功連結它們。extern "C"用來在C++環境下設定C連結型別。
 */
extern "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
    {
		/* 
		 * $1是非終結符tokenlist的屬性,由於該終結符是用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,
		 * $1相當於$1.m_sId,其值已經在下層產生式中賦值(tokenlist IDENTIFIER)
		 */
        cout<<"all id:"<<$1<<endl;    
    };
	
// 記號流,或者為空,或者由若干數字、識別符號、及其它符號組成
tokenlist:
    {
    }
    | tokenlist INTEGER
    {
		/* $2是記號INTEGER的屬性,由於該記號是用%token<m_nInt>定義的,即約定對其用YYSTYPE的m_nInt屬性,$2會被替換為yylval.m_nInt,已在lex裡賦值 */
        cout<<"int: "<<$2<<endl;
    }
    | tokenlist IDENTIFIER
    {
		/* 
		 * $$是非終結符tokenlist的屬性,由於該終結符是用%type<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$$相當於$$.m_sId,
		 * 這裡把識別到的識別符號串儲存在tokenlist屬性中,到上層產生式裡可以拿出為用
		 */
        $$+=" " + $2;
		// $2是記號IDENTIFIER的屬性,由於該記號是用%token<m_sId>定義的,即約定對其用YYSTYPE的m_sId屬性,$2會被替換為yylval.m_sId,已在lex裡賦值
        cout << "id: "<< $2 <<endl;
    }
    | tokenlist OPERATOR
    {
		// $2是記號OPERATOR的屬性,由於該記號是用%token<m_cOp>定義的,即約定對其用YYSTYPE的m_cOp屬性,$2會被替換為yylval.m_cOp,已在lex裡賦值
        cout<<"op: "<<$2<<endl;
    };

%%

// 當yacc遇到語法錯誤時,會回撥yyerror函式,並且把錯誤資訊放在引數s中
void yyerror(const char *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;
}

參考:
https://www.cnblogs.com/rednodel/p/4500388.html