1. 程式人生 > >【原創】Flex和Bison中巧用單雙引號提升語法檔案的可讀性

【原創】Flex和Bison中巧用單雙引號提升語法檔案的可讀性

使用Win Flex 和 Bison有一段時間了,期間搞了幾個小型語言的編譯器,也整理了C和C++的語法檔案,在使用過程中我發現,如果按照傳統的%token標記,將運算子,如“+”、“-”、“*”、“/”等搞成文字記號,比如:%token PLUS、%token MINUS,在宣告語法時,就會出現像下面這樣的定義:

simple_exp       : simple_exp PLUS simple_exp
                | simple_exp MINUS simple_exp;

這樣一來感覺可讀性不是很高,尤其是在像C++這樣的大型語言的語法檔案中,如果所有符號都被這種文字記號定義代替,閱讀、理解、修改都成了比較恐怖的噩夢,就連簡單的“,”、“;”、“{}”等等記號都要被替換成文字,滿篇的全字母語法定義,翻個頁看著都頭暈。此時另一個比較傳統的做法是,對於單字元運算子不要定義記號,直接在yylex函式中返回字母即可,這需要在lex檔案中如下定義:

"+"     |
"-"     |
"*"     |
"/"     |
"%"         |
"^"     {return yytext[0];}  //直接返回字母本身

然後在yacc(Bison)檔案中如下定義語法:

.......
%left '+' '-'
%left '*' '/' 
.......
exp : exp '+' exp {$$ = $1 + $3;}
	| exp '-' exp {$$ = $1 - $3;}
	| exp '*' exp {$$ = $1 * $3;}
	| exp '/' exp {$$ = $1 / $3;}
    ;

這樣一來這段語法定義的含義一眼就看明白了,可讀性比之前的全記號字母方式的定義要高很多,毫不誇張的說但凡寫過程式的人基本都能看懂這個語法檔案。同時這種寫法,也避免了,開頭一堆的%token宣告,有效縮短了檔案長度。但是遺憾的是,這種寫法只能用來應付單字母符號的語法定義,對於像“++”、“–”、“>=”之類的多字母符號,就無能為力了。

正所謂“山窮水盡疑無路,柳暗花明又一村。”,其實在Bison中提供了定義記號時宣告等價字串的功能,這樣我們就可以將所有的%token符號宣告使用其等價字串替代的方式,一方面保留標點符號串原型提高可讀性的,另一方面又可以保留記號定義本身,方便在Flex中讀取不同的幾個符號返回同一個記號值,具體做法如下:

首先在yacc(Bison)語法檔案的%token定義中這樣定義:

%token ASSIGN       ":=" 
%token EQ       "=="
%token LT       "<" 
%token LE       "<="
%token GT       ">"
%token
GE ">=" %token NE "!=" %token PLUS "+" %token MINUS "-" %token TIMES "*" %token OVER "/" %token POW "^" %token MOD "%" %token LPAREN "(" %token RPAREN ")" %token SEMI ";"

這裡需要注意的就是記號的等價字串,必須使用雙引號“”,這樣我們看到所有的不論單字母還是雙字母多字母的標點符號都可以明確的定義,宣告本身也提高了可讀性。具體宣告語法時,就可以像下面這樣使用這些記號:

......

stmt    : compound_stmt { $$ = $1;}
	| if_stmt { $$ = $1;}
	| repeat_stmt ";" { $$ = $1;}
	| assign_stmt ";" { $$ = $1;}
	| read_stmt ";" { $$ = $1;}
	| write_stmt  ";" { $$ = $1;}
	| error { $$ = NULL;}
	;
......
exp : simple_exp "<" simple_exp { $$ = MakeExpNode($1,$3,LT);}
	| simple_exp "==" simple_exp { $$ = MakeExpNode($1,$3,EQ);}
	| simple_exp ">" simple_exp { $$ = MakeExpNode($1,$3,GT);}
	| simple_exp "<=" simple_exp { $$ = MakeExpNode($1,$3,LE);}
	| simple_exp ">=" simple_exp { $$ = MakeExpNode($1,$3,GE);}
	| simple_exp "!=" simple_exp { $$ = MakeExpNode($1,$3,NE);}
	| simple_exp { $$ = $1;}
	;
.......
simple_exp : simple_exp "+" simple_exp { $$ = MakeExpNode($1,$3,PLUS);}
	| simple_exp "-" simple_exp { $$ = MakeExpNode($1,$3,MINUS);}
	| simple_exp "*" simple_exp { $$ = MakeExpNode($1,$3,TIMES);}
	| simple_exp "/" simple_exp { $$ = MakeExpNode($1,$3,OVER);}
	| simple_exp "%" simple_exp { $$ = MakeExpNode($1,$3,MOD);}

	| simple_exp "^" simple_exp { $$ = MakeExpNode($1,$3,POW);}
	|  "(" simple_exp ")" { $$ = $2; }
	| NUM { $$ = MakeConstNode( atoi(yytext) );}
	| ID { $$ = MakeIDNode(yytext);}
	|error { $$ = NULL;}
    ;
......

這樣一來,整個語法檔案看上去就一目瞭然,符號、語義含義都比較清晰了。當然需要重點注意的是,在使用時一樣使用的是雙引號,其實這裡也很好理解,如果你熟悉C語言的話,就知道,在C語言中,單引號只能用來宣告單字元常量,而雙引號則用來宣告字串,同樣在與C語言有著千絲萬縷聯絡的yacc(Bison)檔案中也有類似的單雙引號用法上的區別,這裡明顯使用的是字串。在修改調優較大型的語言如C++這樣量級的語法檔案時,這樣的寫法,會大大提高可讀性,而可讀性是理解和修改整個語法檔案的基礎。舉例來說,我在C++語法檔案中像下面這樣定義(注意其中混用了單雙引號):

postfix_expression : primary_expression
    | postfix_expression '[' expression ']'
    | postfix_expression '(' expression_listopt ')'
    | simple_type_specifier '(' expression_listopt ')'
    | postfix_expression '.' templateopt domainopt id_expression
    | postfix_expression "->" templateopt domainopt id_expression
    | postfix_expression '.' pseudo_destructor_name
    | postfix_expression "->" pseudo_destructor_name
    | postfix_expression "++"
    | postfix_expression "--"
    | DYNAMIC_CAST '<' type_id '>' '(' expression ')'
    | STATIC_CAST '<' type_id '>' '(' expression ')'
    | REINTERPRET_CAST '<' type_id '>' '(' expression ')'
    | CONST_CAST '<' type_id '>' '(' expression ')'
    | TYPEID '(' expression ')'
    | TYPEID '(' type_id ')'
    ;
......

這看起來就像一段C++程式碼本身一樣,任何一個懂C++語言的人看到這樣的語法宣告時都會立即明白其含義,這對於大型的yacc(Bison)語法宣告檔案來說是至關重要的,因為編寫編譯器的首要目標就是正確性,而可讀性是這一切的基礎。

當然,對於使用雙引號字串作為記號等價物時,與使用單個字元不同,在Bison內部,為每個記號和等價字串都生成了相同的狀態值,通常是從258開始編號的,而使用單引號的單字元符號時,生成的狀態值是記號本身的ASCII碼值,通常<=256。在閱讀Bison生成的狀態機檔案或最終程式碼檔案中,需要注意這個區別。

最終在具體使用中,個人推薦使用雙引號字串記號等價宣告的方式,在語法檔案中直接嵌入標點符號終結符這種方式的,因為這為那些有多個不同符號表示相同含義的場合,可以方便的在lex檔案中為不同的符號串返回相同的記號值,比如“!=”和“<>”這樣的都可以表示不等於的情形下,在yacc中可以宣告:

%token NE "!="

在Lex檔案中就可以像下面這樣處理:

"!="        |
"<>"        {return NE;}

最終在語法檔案中引用字串“!=”即可。當然這種技巧在實際的語言語法檔案中不推薦使用,不同的符號串表達相同的含義,這本身就是比較多餘的語言語法設計,對於最終語言的使用者來說無疑只是增加了學習和使用的負擔。同時在複雜的可過載運算子的語言中,這也反倒會降低程式碼的可讀性和簡潔性,試想之前的例子如果允許“!=”和“<>”都能過載,那麼為了可寫性,程式設計師不得不為這兩個運算子都編寫內容重複的過載運算子函式,這顯然也不利於程式碼的維護性。