如有錯誤,望君指正。
1,bison簡介
參考wiki所知,bison是一個GNU自由軟件,用於自動生成語法分析器。根據自定義的語法規則,你可以分析大部分語言的語法,小到桌面計算器,大到復雜的編程語言。想要全面了解bison各個部分,參考bison官方文檔
http://www.gnu.org/software/bison/manual/bison.html
2,基礎知識
2.1 GLR 分析器
bison確定性LR(1)算法在某一特定語法規則點上,不能決定這一步是歸約還是移近。例如:
expr: id | id '+' id;
這樣在第一個id的時候,不知道是歸約還是移近'+'。這就是眾所周知的reduce/rduce和shift/reduce沖突。因為語法改成LR(1)比較復雜,所以GLR分析器運用而生。它大概的原理是,當遇到沖突的時候,那么幾條路都會試着走,試着走是不執行動作。如果只有一條路走通了,那么就執行動作,舍棄其它路子;如果都沒有走通,那么就報語法錯誤;如果多余一條都走通,把相同規約合並,bison可能根據動作優先級來執行,也可能都會執行。
2.2 終結符、非終結符
終結符也就是token,從yylex返回的類型,也是語法樹結構的葉子節點。一般用大寫字母表示。非終結符用於編寫語法規則,也是語法樹結構的非葉子節點,一般用小寫字母表示。
2.3 語法樹
講語法結構抽象成樹形表示。
例如:1+2*3
表達成樹形結構為:

3,語法結構
%{
Prologue
%}
Bison declarations
%%
Grammar rules
%%
Epilogue
'%{'和'%}'之間是c/c++語言的頭文件、全局變量、類型定義的地方,還需要在這里定義詞法分析器yylex和錯誤打印函數yyerror。
'%}'和'%%'之間是bison聲明區間。在這里你需要定義你之后要用到的終結符、非終結符的類型,操作符的優先級。
'%%'和'%%'之間是bison語法規則定義。后文會結合例子來講解。
第二個'%%'之后是c/c++代碼,需要定義Prologue區域的函數,或者其它代碼,生成的c/c++文件會完全拷貝這部分代碼。
4,事例
課程(鏈接)自帶的linux已經安裝好了bison,如果你用自己的linux版本,那么需要yum或apt-get來安裝。
4.1 簡單計算器
一個最簡單的計算器,只有整數的加減法。介紹完整的一個bison文件的各個部分。
Prologue代碼:
%{
//#define YYSTYPE double
#include <ctype.h>
#include <stdio.h>
int yylex (void);
void yyerror (char const *);
%}
你可以發現我注釋掉了YYSTYPE的定義,YYSTYPE指定所有token的語義值類型,如果沒有定義,那么默認為int。這個例子是整數計算器,所以就用默認值。#include引用c頭文件。聲明yylex和yyerror函數,這兩個函數是必須的,因為c語言需要在用到這個函數之前需要提前聲明。yylex函數分解token,返回每個token的類型。yyerror函數輸出錯誤信息。
bison聲明代碼:
%}
%token NUM
%left '+' '-'
%%
聲明一個終結符NUM。
設置'+'和'-'是左結合的。例如:a+b-c,左結合下優先a+b,結果再-c。右結合則優先b-c。
語法規則代碼:
%%
input:
/* empty */
| input line
;
line:
'\n'
| expr '\n' { printf ("%d\n", $1); }
;
expr:
NUM { $$ = $1; }
| expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 - $3; }
;
%%
'/*'與'*/'之間是注釋,'|'是或的意思。我們先看第一段代碼:
input:
/* empty */
| input line
;
這段的意思是:一個完整的input,要么是空(什么也沒有),要么就是這個input后面跟着一個line。所以input可以推出形如"input line line..."這樣的結構。也就說這是一個左遞歸結構。
第一個規則為空,是因為':'和'|'中間什么也沒有(注釋和空白符不算)。input和line都是非終結符。這個完整的規則后需要加';'來代表此規則定義結束。
line:
'\n'
| expr '\n' { printf ("%d\n", $1); }
;
這段的意思是:一個完整的line,要么是'\n'換行符,要么是expr后加一個'\n'換行符。expr是非終結符。line是由兩個規則組成,每個規則后面如果有動作,那么用一對大括號包圍。動作是c/c++代碼。如第二個規則的動作的意思是:如果line推出expr '\n',那么把expr的值打印出來。$1代表的是expr的語義值。語義在這里是整數。$后數字分表代表這個規則的第幾項。如$2就代表'\n',不過$2是沒有意義的。$$代表line的語義值,這里僅僅是打印出expr的值。
expr:
NUM { $$ = $1; }
| expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 - $3; }
;
這段的意思是:一個完整的expr,要么是終結符NUM,要么是expr 加上 expr,要么是expr 減去 expr。當expr是一個整數NUM,那么把這個整數賦值給expr。當expr推出expr '+' expr,那么把加法的結果給$$。因為expr是非終結符,所以expr推導直到NUM為止。減法類似。這是一個遞歸結構,例如這樣一個語句:1+3-2。根據規則expr->NUM先歸約expr+3-2,然后規約'+',繼續歸約終結符為expr+expr-2。因為左結合,根據expr->expr + expr,得expr-2。繼續歸約終結符的expr-expr,最后結果為expr,歸約結束。
%%
int yylex (void)
{
int c;
/* Skip white space. */
while ((c = getchar ()) == ' ' || c == '\t')
continue;
/* Process numbers. */
if (c == '.' || isdigit (c))
{
ungetc (c, stdin);
scanf ("%d", &yylval);
return NUM;
}
/* Return end-of-input. */
if (c == EOF)
return 0;
/* Return a single char. */
return c;
}
/* Called by yyparse on error. */
void yyerror (char const *s)
{
fprintf (stderr, "%s\n", s);
}
int main (void)
{
return yyparse ();
}
yylex函數讀取輸入,如果是數字,就賦值到yylval。因為剛才定義的數字的語義類型為int,所以yylval就是int型。bison的語法規則里用到的形如$1的語義值,就是在yylex函數里賦值的。yylex略去非數字部分,直到文件結尾結束。
yyerror函數直接輸出bison默認錯誤字符串"syntax error"。你可以根據錯誤類型自定義錯誤提示。當錯誤提示返回后,你需要從錯誤中恢復,這在下面的例子中講到。這個例子沒有做恢復處理,所以一旦有語法錯誤,就直接退出程序。
main函數直接調用yyparse進行語法分析過程。
編譯、運行:
編譯bison語法文件,輸入命令:bison calculation.y
沒有報錯的話,生成文件:calculation.tab.c
編譯c文件,輸入命令:gcc calculation.tab.c
沒有錯誤的話,生成可執行文件:a.out
運行,輸入命令:./a.out
輸入:1+1
輸出:2
輸入:1+2-1-2
輸出:0
輸入:1++2
輸出:syntax error
程序退出
4.2 計算器2.0版本
加強版本。支持浮點數運算,加入乘除法,加入指數運算。介紹操作符優先級等。
calculation2.0.y代碼:
https://github.com/YellowWang/bison/blob/master/calculation2.0/calculation2.0.y
Prologue代碼:
Prologue代碼:
%{
#define YYSTYPE double
#include <ctype.h>
#include <stdio.h>
#include <math.h>
int yylex (void);
void yyerror (char const *);
%}
定義YYSTYPE為double,默認所有終結符、非終結符的語義類型為double類型。
bison聲明代碼:
%token NUM
我們先略過error不看。exprs是由expr ';'或 exprs expr ';'組成。也就是說一個表達式集合,是由一個或多個表達式后跟';'組成。$$ = t_single_exprs($1);動作的意思是創建只有一個表達式expr的表達式集,賦值給exprs。$$ = t_append_exprs($1, $2);動作的意思是把表達式expr加入到exprs集合里。Execute($1);的意思是執行這個表達式。這里執行的意思是計算這個表達式的語義值,輸出結果。動作的詳細代碼稍后解析。
%left '+' '-'
%left '*' '/'
%right NEG
%right '^'
%token定義NUM為終結符,此處和上例一樣。
%left是左結合,%right就是右結合,后面跟着操作符,用空格隔開。定義在下方的操作符比上方的操作符優先級更高。一行定義內的操作符之間的優先級是一樣的。由此得出,乘除優先級大於加減,NEG是非,優先級大於乘除,指數運算大於之前所有。
語法規則部分代碼:
expr:
NUM { $$ = $1; }
| expr '+' expr { $$ = $1 + $3; }
| expr '-' expr { $$ = $1 - $3; }
| expr '*' expr { $$ = $1 * $3; }
| expr '/' expr { $$ = $1 / $3; }
| '(' expr ')' { $$ = $2; }
| expr '^' expr { $$ = pow($1, $3); }
| '-' expr %prec NEG { $$ = -$2; };
;
加減乘除值之前寫法差不多。有'('')'的,語義值為括號里面的值。指數乘法用到c數學庫pow函數。下來我們來看看這條規則
'-' expr %prec NEG { $$ = -$2; };
在bison里,一個操作符不能定義兩個不同的優先級,所以'-'已經用作減法的優先級,就不能再用來做負號的優先級。為了解決這個問題,在bison里,先定義操作符NEG的優先級,然后通過 %prec NEG來指定'-'在這個規則為NEG相同的優先級。那么如1--1結果為2。
編譯的時候需要鏈接數學庫(gcc calculation2.0.tab.c -lm),不然提示pow未定義。
運行:
輸入:(-1+3)*5-2^3
輸出:2.00000
4.3 計算器3.0版本
終極版本。除了含有之前版本的功能外,加入大小判斷、if語句、while語句、賦值語句,有簡單語言的雛形。
calculation3.0.y代碼:
https://github.com/YellowWang/bison/blob/master/calculation3.0/calculation3.0.y
bison聲明代碼:
bison聲明代碼:
%union{
Expressions* expressions;
Expression* expression;
char name[32];
double num;
}
%token ASSIGN 258
%token<num> DOUBLE_CONST 259
%token<name> IDENTIFIER 260
%token IF 261 THEN 262 ELSE 263 FI 264
%token WHILE 265 LOOP 266 POOL 267
%right ASSIGN
%nonassoc '<'
%left '+' '-'
%left '*' '/'
%right NEG
%right '^'
%type<expression> expr
%type<expressions> exprs
%type<expressions> exprs_no
可以發現這次比較復雜,我們逐一講解。
%union{
Expressions* expressions;
Expression* expression;
char name[32];
double num;
}
我們之前的例子,所有的終結符、非終結符的語義類型都是一樣的,或整型或浮點型。不過這個例子,會有多種語義類型。%union后大括號里面,每種類型是一個c方式的類型定義。這里有四種類型,分別是Expressions*,Expression*,char[32],double。因為name是32字節,所以變量名不能超過這個大小。先不用管這些是什么,做什么用,等我接下來慢慢道來。
%token ASSIGN 258
%token<num> DOUBLE_CONST 259
%token<name> IDENTIFIER 260
%token IF 261 THEN 262 ELSE 263 FI 264
%token WHILE 265 LOOP 266 POOL 267
%type<expression> expr
%type<expressions> exprs
%type<expressions> exprs_no
%token定義終結符。后面跟着數字代表終結符的編號。%token ASSIGN 258 表示ASSIGN終結符的編號為258。bison會轉化成#define ASSIGN 258。這個和yylex函數返回的類型和編號要保持一致(也可以不指定編號,有的話更方便和lex的宏對應)。終結符的類型通過"%token<類型名> 終結符"這樣的格式來確定。所以DOUBLE_CONST的類型名是num,也就是double類型。IDENTIFIER的類型名是name。關鍵字終結符不需要類型,所以屬於默認類型,也就是YYSTYPE所定義的類型int。
%type是指定非終結符的類型,用法和%token一樣,不過不需要指定編號。我們可以發現expr是expression類型。這里expr的意思是一個表達式,exprs和exprs_no是多個表達式集合。
%nonassoc '<'
%left '+' '-'
%left '*' '/'
%right NEG
%right '^'
這次多了一個新玩意%nonassoc,意思是后面的操作符是沒有結合性的,所以只能是a<b,而不能為a<b<c,這樣bison就無法分辨先是a<b還是b<c。
語法規則代碼:
input:
/* empty */
| exprs
;
一個完整的輸入input是由空或exprs組成。
exprs:
error { $$ = 0;}
| exprs error
| expr ';'
{
$$ = t_single_exprs($1);
Execute($1);
}
| exprs expr ';'
{
$$ = t_append_exprs($1, $2);
Execute($2);
}
;
expr:
IDENTIFIER { $$ = t_id($1); }
| DOUBLE_CONST { $$ = t_num($1);}
| expr '+' expr { $$ = t_plus($1, $3); }
| expr '-' expr { $$ = t_sub($1, $3); }
| expr '*' expr { $$ = t_mul($1, $3); }
| expr '/' expr { $$ = t_div($1, $3); }
| '(' expr ')' { $$ = $2;}
| '{' exprs_no '}' { $$ = t_block($2);}
| expr '<' expr { $$ = t_less($1, $3); }
| expr '=' expr { $$ = t_eq($1, $3); }
| IDENTIFIER ASSIGN expr { $$ = t_assign($1, $3); }
| IF expr THEN expr ELSE expr FI { $$ = t_if($2, $4, $6); }
| WHILE expr LOOP expr POOL { $$ = t_while($2, $4); }
;
一個expr表達式可以是一個IDENTIFIER變量,或是一個浮點數。加減乘除括號和之前例子一樣。所有的動作將在稍后講述。
'{' exprs_no '}' { $$ = t_block($2);}是類似cool語言的一個語法規則:一個表達式可以推出大括號包圍的表達式集合。這個集合類似之前的exprs,區別是exprs_no不需要立即執行表達式的值。因為可能條件判斷不符合,所以這段代碼就不能執行。
'{' exprs_no '}' { $$ = t_block($2);}是類似cool語言的一個語法規則:一個表達式可以推出大括號包圍的表達式集合。這個集合類似之前的exprs,區別是exprs_no不需要立即執行表達式的值。因為可能條件判斷不符合,所以這段代碼就不能執行。
expr '<' expr { $$ = t_less($1, $3); }是比較,如果第一項小於第三項,那么結果為1,否則結果為0。
expr '=' expr { $$ = t_eq($1, $3); }如果第一項等於第三項,那么結果為1,否則為0。(注:此'='沒有賦值的意思。)
IDENTIFIER ASSIGN expr { $$ = t_assign($1, $3); } 賦值語句。表達式可以給一個變量賦值,類似cool語言的語法規則,形如:abc <- 1+1。(ASSIGN就是'<-'符號,通過flex定義,稍后會講到)
IF expr THEN expr ELSE expr FI { $$ = t_if($2, $4, $6); }條件語句。如果第二項成立,那么就進行第四項,否則進行第六項,以FI結尾。
WHILE expr LOOP expr POOL { $$ = t_while($2, $4); }循環語句。如果第二項成立,那么就執行第四項,接着檢測第二項,如此反復,直到推出循環。
exprs_no:
expr ';'
{
$$ = t_single_exprs($1);
}
| exprs_no expr ';'
{
$$ = t_append_exprs($1, $2);
}
;
exprs_no和exprs的語法是一樣,只是動作少了執行。
現在再來看看錯誤處理:
exprs:
error { $$ = 0;}
| exprs error
如果在某一個規則下匹配不出結果,那么就用error來代替。這個規則的意思是:一個完整的表達式集,要么是一個錯誤表達式,要么是一個完整表達式跟着一個錯誤。
實踐運行:
輸入:1 <- 1;
輸出:syntax error
輸入:1**;
輸出:syntax error
如果不進行處理,那么程序會直接退出。
bison的語法文件到此結束,是不是覺得少了yylex函數定義?這次的詞法分析比較復雜,所以用了之前課程學到的flex詞法分析器。
詞法分析器需要提供token的類型,和每個類型的語義值。我們來看看flex文件代碼。
token定義部分代碼:
#define ASSIGN 258
#define DOUBLE_CONST 259
#define IDENTIFIER 260
#define IF 261
#define THEN 262
#define ELSE 263
#define FI 264
#define WHILE 265
#define LOOP 266
#define POOL 267
typedef union YYSTYPE
{
Expressions* expressions;
Expression* expression;
char name[32];
double num;
}YYSTYPE;
extern YYSTYPE yylval;
是不是可以發現這里的終結符的編號和bison里面定義是一致的,出差錯了的話就對應不上,導致語法分析錯亂。
YYSTYPE內的4個類型需要和bison %union里面定義的內容是一致的(在flex沒有用到的類型可以不寫,不過保持一致比較好查錯)。
extern YYSTYPE yylval;調用flex的內置變量yylval,之后要設置需要的token的語義值。
flex定義段代碼:
DIGIT_INT [0-9]+
DIGIT_DOUBLE [0-9]*\.[0-9]+
NOTATION \;|\{|\}|\(|\)|\+|\-|\*|\/|<|=
ASSIGN <-
BLANK \f|\r|\ |\t|\v
NEWLINE \n
IF (?i:if)
ELSE (?i:else)
WHILE (?i:while)
THEN (?i:then)
FI (?i:fi)
LOOP (?i:loop)
POOL (?i:pool)
IDENTFIER [a-zA-Z][a-zA-Z0-9_]*
數字分為整型和浮點數。符號和空白、換行和之前例子一樣。關鍵字如if、else等都是大小寫皆可。
flex規則段代碼:
{DIGIT_INT} {/*ECHO;*/
yylval.num = atof(yytext);
return DOUBLE_CONST;}
{DIGIT_DOUBLE} {/*ECHO;*/
yylval.num = atof(yytext);
return DOUBLE_CONST;}
{NOTATION} { /*ECHO*/; return yytext[0];}
{BLANK} { /*ECHO*/; }
{NEWLINE} { /*ECHO*/; }
{ASSIGN} {/*ECHO*/; return ASSIGN;}
{IF} {/*ECHO;*/ return IF;}
{ELSE} {/*ECHO;*/ return ELSE;}
{WHILE} {/*ECHO;*/ return WHILE;}
{THEN} {/*ECHO;*/ return THEN;}
{FI} {/*ECHO;*/ return FI;}
{LOOP} {/*ECHO;*/ return LOOP;}
{POOL} {/*ECHO;*/ return POOL;}
{IDENTFIER} {/*ECHO*/; strcpy(yylval.name, yytext);
return IDENTIFIER; }
.
如果是數字,那么就轉為double型,賦值給語義值。操作符直接返回字符,關鍵字返回相應的類型。IDENTIFIER變量名賦值給語義name。
flex文件到此結束,生成的c文件有yylex函數提供給bison。
程序運行,
輸入:a <- 1;
輸出:1.00
輸入:a <- a + 1;
輸出:2.00
輸入:a;
輸出:2.00
在這個程序里,變量的值是一直保存的。不過沒有局部變量的含義,你可以認為全部都是全局變量。下面介紹一下做法。
在這個程序里,變量的值是一直保存的。不過沒有局部變量的含義,你可以認為全部都是全局變量。下面介紹一下做法。
全局變量定義代碼:
EXPR_DATA g_symbols[100];
在execute函數里,先計算表達式的值,然后把這個值保存在全局符號表里。
int g_symnum;
EXPR_DATA是結構體,有兩個成員,一個是符號變量名,一個是double型數值。這里定義一個全局符號和數值的對應表g_symbols,簡單起見,最多只能保存100個變量。g_symnum保存當前不同變量的數目。
這個文件還有兩個函數定義:
void SetValue(char* name, double num);
double GetValue(char* name);
SetValue是設置某一個變量的數值;Getvalue得到某一變量的數值。
SetValue是設置某一個變量的數值;Getvalue得到某一變量的數值。
之前的bison語法文件里的動作一筆帶過,這里要詳細講一下。
這段代碼用到了c++的類,每一種表達式都有一個類對應,如:expr '+' expr,對應Expr_plus類;WHILE expr LOOP expr POOL對應Expr_while類等等。這些類都繼承自一個表達式基類Expression,也就是bison語法文件 %union里的類型之一。基類Expression有一個虛函數為execute,意為執行,也就是執行這個表達式的結果。所以每種表達式子類都必須實現這個接口,其實也就是實現這種表達式的語義,返回結果數值。用類的方式組織的主要原因是繼承的結構類似語法樹,可能更方便的對應起來。
舉個例子:在bison語法文件里,這條規則WHILE expr LOOP expr POOL { $$ = t_while($2, $4); },t_while代碼:
Expression* t_while(Expression* con, Expression* e)
{
return new Expr_while(con, e);
}
注:此處new之后並沒有釋放,所以這是一個內存泄露版本。可以在表達式執行后進行釋放。
t_while函數創建並返回一個while表達式類,接受兩個參數,一個是條件表達式,一個是循環體表達式。我們來看下這個類:
class Expr_while : public Expression
{
public:
Expr_while(Expression* con, Expression* e)
{
m_con = con;
m_e = e;
}
virtual double execute()
{
if (!m_con || !m_e)
return 0;
double ace = m_con->execute();
while (!float_eq(ace, 0))
{
m_e->execute();
ace = m_con->execute();
}
return 0;
}
protected:
Expression* m_con;
Expression* m_e;
};
在函數execute里,首先要判斷是否指針是有效的,因為如果某一步語法錯誤的話,這個指針就被賦值為空(NULL)。然后先執行條件表達式,如果非0,那么就執行循環體,然后再執行條件表達式,如此往復。這和c語言的循環方式一樣。
我們再來看看加法的表達式類:
class Expr_plus : public Expression
{
public:
Expr_plus(Expression* e1, Expression* e2)
{
m_e1 = e1;
m_e2 = e2;
}
virtual double execute()
{
if (!m_e1 || !m_e2)
return 0;
double num1 = m_e1->execute();
double num2 = m_e2->execute();
return num1 + num2;
}
protected:
Expression* m_e1;
Expression* m_e2;
};
加法execute函數里面,大意就是分別執行兩個加數的表達式,把兩個返回結果數值加起來返回。
最后看一下賦值表達式代碼:
class Expr_assign : public Expression
{
public:
Expr_assign(char* name, Expression* e)
{
m_name[0]=0;
if (name)
strcpy(m_name, name);
m_e = e;
}
virtual double execute()
{
if (m_name[0]==0 || !m_e)
return 0;
double ace = m_e->execute();
SetValue(m_name, ace);
return ace;
}
protected:
char m_name[32];
Expression* m_e;
};
剩下的表達式類和以上例子差不多,大家可以自己去github上看看。
最終全部編譯和運行:
因為有很多文件組織一起,所以就寫了一個Makefile,現在編譯只要輸入make clean命令,清除生成文件,然后再輸入make進行編譯。關鍵的編譯命令如下:
flex cal.flex
bison calculation3.0.y
$(CC) -c $(CCARG) symtab.c
$(CC) -c $(CCARG) exprtree.cpp
$(CC) -c $(CCARG) lex.yy.c
$(CC) -c $(CCARG) calculation3.0.tab.c
$(CC) -o cal $(CCARG) $(OBJ)
$(CC)代表g++,$(CCARG)代表-g,包含調試信息。最后把所有.o目標文件鏈接成可執行程序cal。運行:
輸入:./cal
基本運算測試:
輸入:a <- 2*(5+2)-17/3;
輸出:8.33
條件語句測試:
輸入:if a < 10 then b <- 1 else b <- 2 fi;
輸出:0.00
條件語句的執行結果並不需要返回什么有意義的值,所以為0。
輸入:b;
輸出:1.00
循環語句測試:
輸入:a <- 10;
輸入:b <- 1;
輸入:while 0 < a loop { b <- b * a; a <- a - 1; } pool;
輸入:b;
輸出:3628800.00
測試結束。
5,一些bison的要點
5.1 不是所有規則都必須要有動作
如果某個規則沒有動作,那么默認動作為$$=$1;
exp: NUM /*{ $$=$1;}*/
5.2 使用左遞歸
任何一種推導序列可以用左遞歸或右遞歸,但是應該用左遞歸。因為左遞歸可以保證有限的堆棧空間,而右遞歸會根據元素個數成比例的占用bison棧空間。因為在規則在應用前,所有元素必須先移動到棧上。
5.3 bison位置信息
出現語法錯誤的時候,bison需要給用戶返回錯誤信息和錯誤發生的行列數。這個錯誤的位置是有yylex來提供。本文沒有講到,具體可以參閱官方文檔ltcalc例子。
6,課程作業簡介
做過第一次大作業,就很方便上手這次也就是第二個大作業。關於這次作業的說明和難點、調試等,將在下一篇介紹。