了解Lex和Yacc,对于当下流行的各种解释性编译器原理的理解有很大帮助,同时lex和yacc强大的字符串处理能力,也可以为其他相关方向的应用提供参考。
我所用的Lex和Yacc的实用工具分别是Flex和Bison。刚刚接触,希望大家不吝指教。
Lex有很多个版本,比如AT&T Lex、MKS Lex、Flex、Abraxas pclex、POSIX lex等。当然,flex不是Adobe的那个flex,是'fast lex'的缩写。感兴趣的读者可以查找相关的背景知识。据说AT&T Lex是一个AT&T暑期实行生的写的,不禁汗颜,虽然据说那个词法分析器不是很健全,但是足以让我产生敬畏感了。
Yacc也有几个对应的版本,AT&T yacc、 Berkeley yacc、GNU Bison。其中GNU Bison来源于GNU yacc。
了解词法分析和语法分析的预备知识是“正则表达式”和“语法生成树”这两个概念。更深一些的比如LR文法、上下文无关文法等,这些都是编译原理的基本知识。
Lex通过正则表达式来匹配字符串,并将符合条件的字符串经过分析,反馈信息给语法分析器。
Yacc分析的是用BNF表述的一个上下文无关文法,并且这个文法满足LALR(1)。由此可以看出,yacc只能向右查看一个输入单元,所以在设计移进归约的时候要小心。
Lex和Yacc通过一个union类型的yylval变量来传递信息,用union的好处在于,不同时刻Lex可以传递不同类型的信息给Yacc,比如:传递一个整形的数值、传提一个指针、传递一个浮点类型数值等等。
本文提供的例子过程如下:
当lex遇到变量名时,yacc在符号表nameList中查找,如果有,则返回该变量名对应的结构体 struct list,若没有则在队尾创建一个。
当lex遇到数值型字符串时,将其转换为float型数字,返回给yacc。
当yacc查找到一个内置函数后,查找符号表nameList,通过找到的函数指针执行对应的函数。
---
当符号个数很多时,符号表nameList采用数组会使查询效率降低,采用哈希散列的方式比较好。
下面列出一个具体的例子来说明问题:
tarr.h
#define MAX 256
typedef double (*FUN)(double);
struct list {
char *name; /*保存变量名,本例中可能是函数名*/
FUN fun; /*保存函数指针,当yacc归约为一个函数名(name)时,查找nameList,将name对应的函数指针返回*/
double value; /*变量所赋的值,是函数名的情况下,该值忽略*/
} nameList[MAX];
struct list *getName(const char*);
void addFun(const char*,FUN);
词法分析程序:【test2.l】
%{
#include "test2.tab.h"
#include "tarr.h"
%}
%option noyywrap
%%
[0-9]+("."[0-9]*)? |
([0-9]+)?"."[0-9]+ |
[0-9]+("."[0-9]*)?[eE][+-]?[0-9]+ |
([0-9]+)?"."[0-9]+[eE][+-]?[0-9]+ {
yylval.dval = atof(yytext); /*浮点数*/ /*当找到匹配的数字型字符串时,就将其转换为float型,并通过yylval变量传递给语法分析程序*/
return NUMBER;
}
[ \t] ;
[A-Za-z][A-Za-z0-9]* {
yylval.nameLST = getName(yytext); /*变量名*/ /*对于一个变量名,先查找nameList中是否已经有这个变量,若没有,则添加进nameList中,参考语法分析中的函数getName */
return NAME;
}
"$" { return 0; /*结束符*/}
\n|. return yytext[0];
%%
语法分析程序:【test2.y】
%{
#include "tarr.h"
#include "stdio.h"
#include "string.h"
#include "math.h"
%}
%union {
double dval;
struct list *nameLST;
}
%start list
%token <nameLST> NAME
%token <dval> NUMBER
%left '-' '+' /*优先级声明 低--->高*/
%left '*' '/'
%nonassoc UMINUS
%type <dval> expr
%%
list:
|stmt '\n'
|list stmt '\n'
;
stmt:NAME '=' expr { $1->value = $3; } /*给变量赋值*/
|expr { printf("= %g\n", $1); } /*归约到底,输出结果*/
;
expr: expr'+' expr { $$ = $1 + $3; } /*$$是返回给':'左侧expr的值*/
|expr '-' expr { $$ = $1 - $3; }
|expr '*' expr { $$ = $1 * $3; }
|expr '/' expr {
if($3 == 0.0)
yyerror("divided by zero\n");
else
$$ = $1 / $3;
}
|'-' expr %prec UMINUS { $$ = -$2; }
|'(' expr ')' { $$ = $2; }
|NUMBER {$$=$1;}
|NAME { $$ = $1->value; } /*NAME本身($1)绑定的是一个struct list* */
|NAME'('expr')'{
if($1->fun){$$=($1->fun)($3);} /*如果是函数,则执行这个函数*/
else{printf("%s is not a function\n",$1->name);$$=0.0;}
}
;
%%
double sqrt(double n) /*自定义的内置函数*/
{
return sqrt(n);
}
int main(int argc,char *argv[])
{
addFun("sqrt",sqrt); /*添加自定义的内置函数*/
yyparse();
return 0;
}
struct list *getName(const char *s)
{
struct list *p;
for(p = nameList; p < &nameList[MAX]; p++) {
if(p->name && !strcmp(p->name, s)) /*符号表中有这个变量名,则返回 */
return p;
if(!p->name) {
p->name = strdup(s); /*没有则创建一个*/
return p;
}
}
yyerror("not enough memory\n");
return 0;
}
void addFun(const char* name,FUN fun)
{
struct list *functionItem=getName(name);
functionItem->fun=fun;
}
yyerror(char *s)
{
fprintf(stderr, "error: %s\n", s);
}
makefile文件:
#makfile
test1:test2.l test2.y
bison -d test2.y
flex test2.l
cc -o $@ test2.tab.c lex.yy.c -lfl
运行结果:
frank@ubuntu:~/FlexBison$ make
bison -d test2.y
test2.y: conflicts: 4 shift/reduce
flex test2.l
cc -o test1 test2.tab.c lex.yy.c -lfl
frank@ubuntu:~/FlexBison$ ./test1
a=4
b=50
c=-(3+sqrt(a))+b/5.0
b=1.5
a+b+c
= 10.5
$
frank@ubuntu:~/FlexBison$
通过运行结果可以看到,有一个移进归约冲突,当遇到移进归约冲突时,Bison默认的处理方式是移进。
这个冲突是:
|NAME
|NAME'('expr')'
感兴趣的可以改下。