flex bison 基础概述
1. 前言
限于作者能力水平,本文可能存在謬誤,因此而給讀者帶來的損失,作者不做任何承諾。
2. 本文目標
. 簡單介紹 flex 和 bison 的基礎使用方法 . 簡要分析 flex, bison 生成代碼的工作流程3. flex & bison
3.1 背景
本文所有分析,基于 Ubuntu 16 系統。
3.2 flex
3.1.1 flex 簡介
flex用來生成詞法分析器(lexical analysis, 或 scanner),而詞法分析器的作用,簡單來講,就是將輸入,按定義的正則表示式模式,解析分割成一個個記號(token)。
# 生成詞法分析器flex XXX.l(詞法分析器規則定義文件) ======> 詞法分析器# 通過詞法分析器,將輸入數據流,解析成一個個記號(tokens)詞法分析器 輸入數據流 ===========> 一個個記號(tokens)3.1.2 flex 使用例子
(1) 初次使用,先運行如下命令安裝 flex:
sudo apt-get install flex(2) 編寫 flex 程序(用來生成詞法分析器的規則文件XXX.l)。
我們先來了解一下 flex 程序的編寫規則,flex 程序分為3個部分:
這3個部分,用2個%%分隔,前2個部分是必須的,但它們的內容可以為空,第3部分和它之前的%%可以省略。
了解 flex 程序的編寫規則后,接下來,我們以一個統計字符數、單詞數目、行數的 flex 程序為例,來演示一下flex的使用,flex 程序count-words.l如下:
%{ int chars = 0; /* 字符計數 */ int words = 0; /* 單詞計數 */ int lines = 0; /* 行計數 */ %}%%[a-zA-Z]+ { words++; chars += strlen(yytext); } \n { chars++; lines++; } . { chars++; }%%int main(int argc, char *argv[]) {yylex();printf("%8d%8d%8d\n", chars, words, lines);return 0; }編寫一個簡單的 Makefile 來編譯我們的詞法分析器:
count-words: count-words.lflex --noyywrap count-words.l # 生成詞法分析器代碼 lex.yy.cgcc -o $@ lex.yy.c # 編譯詞法分析器 lex.yy.c clean:-rm -f lex.yy.c count-words編譯和運行:
make # 編譯生成詞法分析器程序 count-words ./count-words # 運行詞法分析器,按 Ctrl + D 結束數據輸入測試中我們發現,詞法分析器從標準輸入接收數據,這是默認的行為。如果我們想改變該默認行為,轉而將文件作為輸入,只需按如下修改 flex 程序count-words.l就可以達到目標:
%option noyywrap%{ /* 統計單個文件數據 */ int chars = 0; int words = 0; int lines = 0;/* 統計所有文件數據 */ int total_chars = 0; int total_words = 0; int total_lines = 0; %}%%[a-zA-Z]+ { words++; chars += strlen(yytext); } \n { chars++; lines++; } . { chars++; }%%int main(int argc, char *argv[]) {int i;if (argc < 2) { /* 沒有給定文件列表,仍然從標準輸入獲取數據 */yylex();printf("%8d%8d%8d\n", chars, words, lines);return 0;}/* 遍歷所有輸入文件,統計每一個文件的數據 */for (i = 1; i < argc; i++) {FILE *fp = fopen(argv[i], "r");if (!fp) {perror(argv[i]);return -1;}/* 復位當前文件的統計數據 */chars = words = lines = 0;yyrestart(fp); /* 調用 yyrestart() 接口重置詞法分析器的輸入流到文件 @argv[i] */yylex(); /* 調用詞法分析器進行數據統計 */fclose(fp); /* 關閉當前文件 */printf("%8d%8d%8d %s\n", chars, words, lines, argv[i]);/* 記錄當前文件的統計數據 */total_chars += chars;total_words += words;total_lines += lines;}if (argc > 1)printf("%8d%8d%8d total\n", total_chars, total_words, total_lines);return 0; }修改后重新編譯運行:
make ./count-words a.txt b.txt # a.txt, b.txt 作為輸入如果不指定輸入(具體是修改yyin全局變量),程序默認使用標準輸入;如果要修改輸入,我們可以通過yyrestart()修改。
3.1.3 flex 生成代碼流程簡析
通常,我們有必要簡單的分析下flex的生成代碼,以幫助我們理解和更好的使用工具。下面簡要分析flex生成代碼的工作流程:
/* * 在分析具體代碼前,我們先聊一下 flex 的 fl 庫。* flex工具帶有一個微型的 fl 庫,它定義了 main(), yywrap() 接口。 其中:* . main() 函數調用 yylex() 做詞法分析;* . yywrap() 簡單地返回 1。yywrap() 的作用是,在yylex()發現到達輸入數據末尾時,調用 yywrap(),看是否還有數據,如果有,yywrap() 應該返回0,否則返回1。** 如果我們的flex程序,不自己實現 main() 和 yywrap(),則在編譯時,可以給 gcc 指定 -lfl 選項。另外,可以通過給 flex 傳遞 --noyywrap 選項,或者在 flex 程序中,指定 %option noyywrap 來告訴flex ,我們不調用 yywrap() ,以此來屏蔽編譯鏈接報錯。*/ /* 接下來,進入具體的代碼流程分析 */ main()yylex() /* 進入詞法分析器入口 *//* yylex() 初次調用的初始化。后續 yylex() 調用會在之前的上下文下繼續工作。 */if ( !(yy_init) ){(yy_init) = 1;...if ( ! (yy_start) )(yy_start) = 1; /* first start state */if ( ! yyin ) /* 沒有設定輸入, 默認將 stdin 作為輸入 */yyin = stdin;if ( ! yyout ) /* 沒有設定輸出, 默認將 stdout 作為輸入 */yyout = stdout;/* 輸入緩沖初始化 */if ( ! YY_CURRENT_BUFFER ) {yyensure_buffer_stack (); /* 創建 yy_buffer_state 輸入緩沖管理對象指針棧 */YY_CURRENT_BUFFER_LVALUE =yy_create_buffer(yyin,YY_BUF_SIZE ); /* 創建棧頂 YY_BUF_SIZE 大小的輸入緩沖 */}/** 獲取棧頂輸入緩沖如下狀態:* . yy_n_chars: 讀到棧頂輸入緩沖空間的字符個數* . yytext, yy_c_buf_p: 棧頂輸入緩沖空間當前位置指針(char *)* . yyin: 輸入緩沖輸入文件* . yy_hold_char: 棧頂輸入緩沖空間當前字符*/yy_load_buffer_state( );}/* 掃描循環,直到輸入結束 */while ( /*CONSTCOND*/1 ) /* loops until end-of-file is reached */{/* 正則匹配狀態機循環 */yy_current_state = (yy_start);yy_match:do {YY_CHAR yy_c = yy_ec[YY_SC_TO_UI(*yy_cp)] ;...yy_current_state = yy_nxt[yy_base[yy_current_state] + (unsigned int) yy_c];++yy_cp;}while ( yy_base[yy_current_state] != 17 );.../* 一個正則模式匹配完成的后慣例動作:* yytext: 當前正則模式匹配的內容* yyleng: 當前正則模式匹配內容長度* yy_hold_char: 當前正則模式匹配內容的最后一個字符(也即當前字符)* *yy_cp = '\0';* yy_c_buf_p: 下一個待解析字符位置指針*/YY_DO_BEFORE_ACTION;do_action: /* This label is used only to access EOF actions. *//* 狀態機正則匹配結束,按匹配的正則做用戶定義的動作 */switch ( yy_act ){ /* beginning of action switch */...case 1:YY_RULE_SETUP #line 18 "calculator.l"{ return ADD; } /* 正則模式匹配時,執行的C代碼 */YY_BREAK...default:YY_FATAL_ERROR("fatal flex scanner internal error--no action found" );}}其實 yylex() 的工作邏輯很簡單,可以總結如下:
while (1)從輸入讀取數據按正則模式匹配輸入數據如果有匹配的模式,執行匹配模式的C代碼否則,報錯注意到,yylex() 是返回值的,不出錯的情形下,它返回匹配模式的 token ,這就是它可以和 bison 生成代碼一起協作的基礎。
3.1.4 flex 小結
上面我們簡單介紹了 flex 的基礎用法,但很多時候,這些并不足夠。下面列舉幾個對我們日常很常見也很有用的 flex 用法。
3.1.4.1 option 選項
(1) 生成可重入詞法分析器。
一方面,生成的詞法分析器代碼,有很多全局變量;
另一方面,詞法分析器入口 yylex(),返回后,下一次調用,會接著使用上一次運行后的上下文繼續執行。
上面兩點,不能滿足要求可重入的調用的上下文,此時,我們可以通過%option reentrant 選項來生成可重入的詞法分析器。此時,我們通過如下代碼片段構建可重入的詞法分析器:
(2) 改變生成代碼函數名。
我們有時候可能不想使用 yylex() 等其它詞法分析器接口名,可以通過%option prefix="XXX"作為詞法分析器接口名前綴。如:%option prefix="parse_events_",那生成代碼中,yylex() 則變為 parse_events_lex(),當然,還有更多函數名的變換。
(3) 自動維護行號代碼。
我們可以自己在規則中,更新 yylineno 來維護行號。當然,也可以通過 %option yylineno選項,讓 flex 幫我們自動生成行號維護代碼。
(4) 與 bison 協同工作選項。
默認生成的代碼,yylex() 函數是沒有參數的,除了可以通過%option reentrant選項來增加詞法分析器的上下文參數外,我們還可以通過%option bison-bridge和%option bison-locations來改變 yylex() 原型:
%option bison-bridge: 為 yylex() 增加參數 YYSTYPE *yylval_param, 用來記錄詞法分析器解析的 token 的值。 %option bison-locations: 為 yylex() 增加參數 YYLTYPE *yylloc_param, 用來存儲行列信息。如果有以下選項配置:
%option reentrant %option bison-bridge %option bison-locations ... %% ... %% ...則生成的 yylex() 函數原型為:
int yylex(YYSTYPE *yylval_param, YYLTYPE *yylloc_param, yyscan_t yyscanner);3.1.4.2 定義
類似于C中的宏定義,主要是將正則規則中重復的部分抽離出來,免得重寫。我們看一個例子:
group [^,{}/]*[{][^}]*[}][^,{}/]* %% {group} {BEGIN(INITIAL);REWIND(0);} %% ...上例中,在第一部分定義了 group ,然后再第二部分規則中引用,使用 {} 括起來。
3.1.4.3 定義特定狀態下才會執行的規則
%x IFILE%%^"#"[ \t]*include[ \t]*[\"<] { BEGIN IFILE; }<IFILE>[^ \t\n\">]+ {{int c;while ((c = input()) && c != '\n');}yylineno++;if (!newfile(yytext))yyterminate(); /* no such file */BEGIN INITIAL;}<IFILE>.|\n {fprintf(stderr, "%4d bad include line\n", yylineno);yyterminate();}<<EOF>> { if (!popfile()) yyterminate(); }^. { fprintf(yyout, "%4d %s", yylineno, yytext); } ^\n { fprintf(yyout, "%4d %s", yylineno++, yytext); } \n { ECHO; yylineno++; } . { ECHO; }%% ...上面我們通過 %x 定義了一個 exclusive 的狀態 IFILE,在該狀態下,只有以 <IFILE> 開頭的規則才會被執行,它用來解析 #include 預處理符號。其中,特殊符號<<EOF>>表示遇到文件結尾;詞法分析器用 input() 從輸入讀取一個字符,yylineno 記錄行號,yytext 記錄當前匹配的文本,ECHO 回顯匹配的文本。
詞法分析器的初始狀態為INITIAL(即0),可以通過YY_START或YYSTATE獲取當前狀態,通過BEGIN來切換當前狀態,如上例中的BEGIN IFILE;來切換詞法分析器的狀態為IFILE,在狀態IFILE下,只有以<IFILE>開頭的規則才會被執行。
另外還可以通過%s定義可共享狀態。假設我們通過%s SS定義了狀態SS,和%x定義不同的是,除了將<SS>開頭的規則限制在只能在SS狀態下執行外,而剩余的其它規則,也可以在SS狀態下執行。
通常來講,%x是更加有用的,因為它將分析限制于特定的上下文,這可以簡化我們詞法分析器的設計。
3.2 bison
3.2.1 bison 簡介
bison基于給定的語法,來生成一個可以識別這個語法中有效語句的語法分析器。我們簡單的看一下語法分析器的生成流程:
# 生成語法分析器bison -d XXX.y ==========> XXX.tab.l, XXX.tab.h# 與詞法分析器協作分析語法tokens----------------| || V詞法分析器 語法分析器^ | | V輸入數據流 合乎語法的語句3.2.2 flex + bison 使用例子
(1) 初次使用,先運行如下命令安裝 bison:
sudo apt-get install bison(2) 編寫 bison 程序。
我們先來說一下 bison 程序(XXX.y)的編寫規則,bison 程序分為3個部分:
這3個部分,用2個%%分隔,前2個部分是必須的,但它們的內容可以為空,第3部分和它之前的%%可以省略。
接下來,我們一個簡單計算器為例,構建計算器的 flex 和 bison 程序如下。
其中 yylval 記錄當前識別到的 token 的值,可以導出給語法分析器使用。
%{ /** calculator.y*/#include <stdio.h>extern int yylex(void);void yyerror(char *s); %}/* declare tokens */ %token NUMBER %token ADD SUB MUL DIV ABS %token OP CP %token EOL%%callist: /* nothing */| callist exp EOL { printf(" = %d\n", $2); };exp: factor| exp ADD factor { $$ = $1 + $3; }| exp SUB factor { $$ = $1 - $3; };factor: term| factor MUL term { $$ = $1 * $3; }| factor DIV term { $$ = $1 / $3; };term: NUMBER| ABS term { $$ = $2 >= 0? $2 : - $2; }| OP exp CP { $$ = $2; };%%int main(int argc, char *argv[]) {yyparse(); }void yyerror(char *s) {fprintf(stderr, "error: %s\n", s); }上面 bison 程序中的 $$, $1,... 等是用描述每條語法規則中語法符號的值,$$ 表示規則左邊符號的值,$1,$2,...依次表示規則右邊第1個,第2個,...符號的值。
接下來編寫用來編譯的 Makefile :
編譯運行:
make # 編譯生成計算器程序 calculator ./calculator # 運行計算器程序,按 Ctrl + D 結束數據輸入3.2.3 flex 與 bison 協作流程簡析
main() /* calculator.tab.c: bison -d 生成的代碼 */yyparse()...yychar = YYEMPTY; /* Cause a token to be read. */goto yysetstate;...yynewstate: /* 下一狀態 *//* In all cases, when you get here, the value and location stackshave just been pushed. So pushing a state here evens the stacks. */yyssp++;yysetstate:...yyn = yypact[yystate];...if (yychar == YYEMPTY){YYDPRINTF ((stderr, "Reading a token: "));yychar = yylex (); /* 調用詞法分析器解析一個token *//* * 參看 3.1.3 小節,詞法分析器工作流程*/}.../* Discard the shifted token. */yychar = YYEMPTY;yystate = yyn;YY_IGNORE_MAYBE_UNINITIALIZED_BEGIN*++yyvsp = yylval;YY_IGNORE_MAYBE_UNINITIALIZED_ENDgoto yynewstate; /* 進入下一分析狀態 */3.2.4 bison 小結
上面我們簡單介紹了 bison 的基礎用法,但很多時候,這些并不足夠。下面列舉幾個對我們日常很常見也很有用的 bison 用法。
3.2.4.1 option 選項
(1) 要求 bison 版本。
%require "2.4" %% ... %% ...(2) 自定義語法分析器入口 yyparse() 函數原型。
默認生成語法分析器入口 yyparse() 函數是沒有參數的,但有時候,我們需要給它傳遞參數,這時我們可以通過%parse-param選項來自定義參數列表。如:
%parse-param {void *_parse_state} %parse-param {void *scanner} %% ... %% ...則 yyparse() 函數的原型定義為:
int yyparse (void *_parse_state, void *scanner);(3) 生成可重入的語法分析器。
%define api.pure %% ... %% ...還可以使用%pure-parser代替%define api.pure。這兩個選項通常結合%parse-param使用,給 yyparse() 傳遞參數。
(4) 定義詞法分析器解析符號的ID。
/* declare tokens */ %token NUMBER %token ADD SUB MUL DIV ABS %token OP CP %token EOL%% ... %% ...上面的 bison 程序,會在 *.tab.h 中對應生成如下的 token 定義:
#ifndef YYTOKENTYPE # define YYTOKENTYPEenum yytokentype{NUMBER = 258,ADD = 259,SUB = 260,MUL = 261,DIV = 262,ABS = 263,OP = 264,CP = 265,EOL = 266}; #endif%token 用來定義 token 編號,給詞法分析器的 token 編號。bison 以 258 為生成符號的起始編號,避免和 ascii 值沖突。另一種 token 編號的使用方法是單引號內含字符的方式,如 ‘+’ ,則用 + 的 ascii 值作為其 token編號,不必額外定義。
(5) 定義語法分析器規則中符號的數值類型。
在語法分析的規則中,有時候需要通過%union指定符號的數值類型。我們看個簡單的例子:
%union {struct ast *a;double d; }/* declare tokens */ %token <d> NUMBER %token EOL%type <a> exp factor term%% ... %% ...其中,用%union聲明被轉化為如下C代碼段:
/* Value type. */ #if ! defined YYSTYPE && ! defined YYSTYPE_IS_DECLAREDunion YYSTYPE { #line 8 "calculator.y" /* yacc.c:1909 */struct ast *a;double d;#line 64 "calculator.tab.h" /* yacc.c:1909 */ };typedef union YYSTYPE YYSTYPE; # define YYSTYPE_IS_TRIVIAL 1 # define YYSTYPE_IS_DECLARED 1 #endif也即定義為 YYSTYPE 類型。如果不聲明%union,則 YYSTYPE 定義為 int 。聲明%token <d> NUMBER表示,語法符號NUMBER的數據類型為,聲明%union中,數據成員d的數據類型double;聲明%type <a> exp factor term表示,語法符號exp,factor,term的數據類型為,聲明%union中,數據成員a的數據類型struct ast *。
語法會存在二義性,簡單來講,語法二義性就是一段輸入,可匹配到不同的語法規則情形。本文對語法的二義性未做描述。
4. 后記
人的精力時間總是有限的,我認為我們學習一樣技能,總是、也應該是出于某種目的的。對于那些有明顯規律的(可用正則表達式和語法描述的)輸入,flex 和 bison 可以極大地提高我們的生產效率、代碼的可維護性。對于更加復雜的輸入,手工編寫的代碼,閱讀、調試困難,可維護度、擴展性極差,這時候應該利用 flex 和 bison,它們都是久經歷史考驗的,只要能正確設計正則規則、語法規則,它們就能幫我們保證程序的正確性和效率。
本文僅對 flex 和 bison 做了簡單基礎性地描述,更多的細節,以及對齊內部的實現原理等方面未做展開,讀者可閱讀后面的資料,進行補充。
5. 推薦閱讀 & 參考資料
參考資料:
《flex & bison》, John R. Levine [flex](https://www.gnu.org/savannah-checkouts/gnu/www/software/flex/flex.html) [GNU Bison](https://www.gnu.org/software/bison/)推薦閱讀:
《A Retargetable C Compiler_Design and Implementation》 《Advanced Compiler Design and Implementation》 《Building an Optimizing Compiler》 《Compiler Construction Principles And Practice》 《Compiler Design in C》 《Compilers Principles Techniques and Tools》 《Crafting a Compiler》 《Engineering a Compiler》 《Introduction to Compiler Construction》 《Language Implementation Patterns》 《Modern Compiler Implementation in C》 《Modern Compiler Design》 《Programming Language Pragmatics》 《The Implementation of Functional Programming Languages》 《計算機程序的構造和解釋》總結
以上是生活随笔為你收集整理的flex bison 基础概述的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数据恢复软件哪个好?强推easyreco
- 下一篇: ansible剧本(playbook)