lex和yacc學習
阿新 • • 發佈:2020-11-20
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; }