一個Lex/Yacc完整的示例(可使用C++)
阿新 • • 發佈:2018-12-26
作者: 胡彥 2013-4-28
程式碼下載地址:http://pan.baidu.com/share/link?shareid=579088&uk=253544182
本框架是一個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