我的好兄弟之Flex&Bison 第二章 讓Flex和Bison一起干活!


前面的話:在此之前,如果我接到一個解析文本的工作,我會逐行讀取並存儲我想要的數據再去處理數據。最近,工作中需要去解析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乘法表了~

加油:)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM