【原创】Flex和Bison中巧用单双引号提升语法文件的可读性

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014038143/article/details/78202271

使用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;}

最终在语法文件中引用字符串“!=”即可。当然这种技巧在实际的语言语法文件中不推荐使用,不同的符号串表达相同的含义,这本身就是比较多余的语言语法设计,对于最终语言的使用者来说无疑只是增加了学习和使用的负担。同时在复杂的可重载运算符的语言中,这也反倒会降低代码的可读性和简洁性,试想之前的例子如果允许“!=”和“<>”都能重载,那么为了可写性,程序员不得不为这两个运算符都编写内容重复的重载运算符函数,这显然也不利于代码的维护性。

猜你喜欢

转载自blog.csdn.net/u014038143/article/details/78202271
今日推荐