前面的話:在此之前,如果我接到一個解析文本的工作,我會逐行讀取並存儲我想要的數據再去處理數據。最近,工作中需要去解析verilog代碼,相信verilog有許多人都用過,各關鍵字有相對應的含義和用法,很明顯不能通過上述的方法來做,大概瀏覽了github,給我這個沒有學過編譯原理的人指出了一條明路:yacc&lex,或者,flex&bison。
本系列文章:我寫這個系列的博客主要是記錄收獲的知識和踩過的坑,初學者的緣故,並不對其他人具有指導意義。當然,也可能你也和我有相同的問題或者感想,我們可以好好討論一下。
我的目標:以后如果碰到需要解析的工作,一個下午搞定。
前章:我的好兄弟之Flex&Bison 第一章 實現人生第一個Flex!
讓Flex和Bison一起干活!
使用Flex和Bison攜手制作一個桌面計算器。首先,編寫一個詞法分析器,然后編寫一個語法分析器並把兩者結合起來。
一、再學一點Flex
第一章只是實現了一個Flex程序,只是皮毛。現在,再學一點皮毛吧~
1、一個簡單計算器的詞法
腦海中的計算器大概就是加減乘除吧,先用Flex進行一個簡單的定義
%% "+" {printf("PLUS\n");} "-" {printf("MINUS\n");} "*" {printf("TIMES\n");} "/" {printf("DIVIDE\n");} "|" {printf("ABS\n");} [0-9]+ {printf("NUMBER %s\n",yytext);} \n {printf("NEWLINE\n");} [ \t] {} . {printf("Mystery character %s\n",yytext);} %%
經過第一章,這幾種模式我就不再贅述了。
然后我們編譯一下,或者直接寫一個Makefile
一定有這樣的疑問:沒有第三段的C代碼,也可以運行嗎?
是的,flex庫文件(-lfl)提供了一個極小的主程序來調用詞法分析器,對這個小例子來說是足夠用的。
2、yylex()
第一章例子中的第三段C代碼中出現了yylex(),那么它是用來干什么的呢?另一個問題,為什么上述的例子中只要換行就會打印信息呢?
每當程序需要一個記號時,它就會調用yylex()來讀取一小部分輸入然后返回相應的記號。當動作代碼識別出一個記號時,yylex()就會將這個記號作為返回值。需要下一個記號時,就會再次調用yylex()。yylex()會記住當前處理的位置,並從此處開始下一次調用。
另外,如果一個模式並不能返回記號時,yylex()會繼續分析接下來的輸入直到有返回記號或者我們讓它停止。
舉一個很簡單的例子:
[0-9]+ {return NUMBER;} \n {return EOL;} [ \t] {}
如果我們輸入中有空格,並不會NUMBER和EOL的記號造成影響。
3、記號編號和記號值
當flex詞法分析器返回一個記號流時,每個記號有兩個組成部分,記號編號(token number)和記號值(token's value)。記號編碼類似枚舉,它是一個整數,並沒有既定規律,但是零意味着文件結束。
直接上代碼
%{ enum yytokentype { NUMBER = 258, ADD = 259, SUB = 260, MUL = 261, DIV = 262, ABS = 263, EOL = 264 }; int yylval; int lexerror(char *s); %} %% "+" { return ADD; } "-" { return SUB; } "*" { return MUL; } "/" { return DIV; } [0-9]+ { yylval = atoi(yytext); return NUMBER; } \n { return EOL; } [ \t] { /* ignore white space */ } . { lexerror(yytext); } %% int lexerror(char *s) { fprintf(stderr, "lexical error: %s\n", s); } int main(int argc, char** argv){ int tok; while(tok = yylex()){ printf("%d",tok); if(tok == NUMBER) printf(" = %d\n",yylval); else printf("\n"); } }
打印來看看
打印的是記號的編號。
二、精力轉向語法分析器
我們已經有了一個詞法分析器了,接下來了解一下語法分析器
1、BNF文法
在計算機分析程序里常用的語言就是上下文無關文法(Context-Free Grammar,CFG)。書寫上下文無關文法的標准格式就是BackusNaur范式。
簡單的來說,BNF文法就是簡潔描述編程語言的語言。它的基本結構為:
<exp> ::= <factor>
| <exp> <factor>
<factor> ::= NUMBER
| <factor> NUMBER
::= 意味着左邊還有東西沒有定義完,還可以由右邊的的表達式繼續定義。
| 意味着或,就是還可以用右邊的表達式繼續定義。
舉個例子:
在中文語法里,一個句子一般由“主語”、“謂語”和“賓語”組成,主語可以是名詞或者代詞,謂語一般是動詞,賓語可以使形容詞,名詞或者代詞。那么“主語”、“謂語”和“賓語”就是非終止符,因為還可以繼續由“名詞”、“代詞”、“動詞”、“形容詞”等替代。
例1. <句子> ::= <主語><謂語><賓語>
例2. <主語> ::= <名詞>|<代詞>
例3. <謂語>::=<動詞>
例4. <賓語>::=<形容詞>|<名詞>|<代詞>
例5. <代詞>::=<我>
2、初探Bison規則描述語言
bison的規則基本上就是BNF,但是做了一點點簡化。直接上例子
%{ #include <stdio.h> %} %token NUMBER %token ADD SUB MUL DIV ABS %token EOL %% calclist : | calclist exp EOL { printf("= %d\n",$2); } ; exp : factor { $$ = $1;} | exp ADD factor { $$ = $1 + $3; } | exp SUB factor { $$ = $1 - $3; } ; factor : term { $$ = $1; } | factor MUL term { $$ = $1 * $3; } | factor DIV term { $$ = $1 / $3; } ; term : NUMBER { $$ = $1; } | ABS term { $$ = $2 >= 0? $2 : -$2; } ; %% main(int argc,char **argv){ yyparse(); } yyerror(char *s){ fprintf(stderr, "error:%s\n", s); }
可以看到,bison程序包含和flex程序相同的三部分結構。
第一部分除了聲明部分還包括%token記號部分,以便告訴bison在語法分析程序中記號的名稱。
第二部分包含通過簡單的BNF定義的規則。bison使用單一的冒號而不是::=,同時使用分號表示規則的結束。每個bison規則中的語法符號都有一個語義值,目標符號的值(冒號左邊)通過$$表示,右邊語法符號的語義值一次為$1、$2,直到這條規則結束。當詞法分析器返回記號時,記號值總是儲存在yylval里,其他語法符號的語義值則在語法分析器的規則里設置。比如本例中factor、term和exp符號的語義值就是它們所描述的表達式的值。
三、一起干活!
1、修改Flex程序
首先先編譯一下bison程序,bison和yacc一致,都是.y結尾的文件。
bison -d fb1-5.y
會創建兩個文件,分別是fb1-5.tab.c和fb1-5.tab.h。
不考慮.tab.c,看看.tab.h文件。
有沒有很像在flex程序的第一部分,記號編號和yylval。
悟了,編譯bison會生成一個.tab.h的文件,我們可以將其通過#inclde<fb1-5.tab.h>引入到.l文件的第一部分,修改一下fb1-5.l的第一部分。
%{ #include"fb1-5.tab.h" int lexerror(char *s); %} %% ...
...
2、進行一個括號的加
細心一點就會發現,目前實現的計算器並沒有括號,速速添加進去
在flex程序里添加
%{ ...... .... %} %% "(" { return LP; } ")" { return RP; } ...... .... %%
在bison程序里添加
%token LP RP %% term : NUMBER { $$ = $1; } | ABS term { $$ = $2 >= 0? $2 : -$2; } | LP exp RP { $$ = $2; } ; %%
3、計算器成功
現在flex和bison對應的fb1-5.l和fb1-5.y都准備就緒了,因為命令變多了,我們寫一個Makefile
fb1-5: fb1-5.l fb1-5.y bison -d fb1-5.y flex fb1-5.l cc -o $@ fb1-5.tab.c lex.yy.c -lfl
運行之
好了,我們的計算器制作完成,缺點就是僅針對整數。
4、遇到的問題
(1)bison規則中的default
(2)關於"%"的小細節
不管是flex程序還是bison程序,並不是必須都要包含三部分。
%{ #include"fb1-5.tab.h" int lexerror(char *s); %} %%
如上圖所示,僅僅這樣也能編譯成功。但是最后一個"%%"是必不可少的,否則就會報錯。
妥了,有了計算器再也不用每天伏案默寫99乘法表了~
加油:)