使用flex和bison,对c语言代码块进行词法分析,识别词法错误,按照c—语法规则进行文法分析,并形成c语言代码块的语法树(syntax tree),并将语法树按照特定的格式打印出来。
两种方法:
(1)使用make命令:先将要执行的所有命令写入到Makefile文件中,然后执行make命令,这就相当于将Makefile中的所有命令都执行完毕了,在终端可以清楚地看到系统每执行一条命令的结果,如果有错或者有警告都会输出。make执行完之后,就生成a.out文件,使用cat filename|./a.out就可以对filename中的文件就行语义分析了。
当文件有更新时,只需要执行以下make即可。
(2)这种是最笨的方法,就是不写Makefile文件,每次都是逐条去执行编译命令,文件有更新之后也要逐条执行,这种效率是最低的。make命令的出现就是为了提高编译效率的,有关make的知识点可以自行百度。
下面是第一种编译方法的具体过程:
程序总共包含4个文件gramtree_v1.h gramtree_v1.c gramtree.l gramtree_v1.y
gramtree_v1.h gramtree_v1.c定义和实现了创建语法树和遍历语法树的函数
gramtree.l是flex词法分析模块
gramtree_v1.y是bison语法分析模块
编译时使用Makefile文件,编写文件内容,vim Makefile
result:gramtree_v1.y gramtree.l gramtree_v1.h
bison -d gramtree_v1.y
flex gramtree.l
gcc gramtree_v1.tab.c lex.yy.c gramtree_v1.c
编译过程如下:
[root@localhost flex]# make
[root@localhost flex]# cat input3.c|./a.out
先执行make命令,这代表Makefile的编译步骤全都执行完毕了。然后使用
cat filename|./a.out就可以对filename中的代码进行语法分析,打印出语法树。
/*
*Name:gramtree_v1.h
*Author:WangLin
*Created on:2015-10-03
*Function:定义语法树结点结构体&变长参数构造树&遍历树函数
*/
/*来自于词法分析器*/
extern int yylineno;//行号
extern char* yytext;//词
void yyerror(char *s,...);//错误处理函数
/*抽象语法树的结点*/
struct ast
{
int line; //行号
char* name;//语法单元的名字
struct ast *l;//左孩子
struct ast *r;//右孩子
union//共用体用来存放ID/TYPE/INTEGER/FLOAT结点的值
{
char* idtype;
int intgr;
float flt;
};
};
/*构造抽象语法树,变长参数,name:语法单元名字;num:变长参数中语法结点个数*/
struct ast *newast(char* name,int num,...);
/*遍历抽象语法树,level为树的层数*/
void eval(struct ast*,int level);
/*
*Name:gramtree_v1.c
*Author:WangLin
*Created on:2015-10-03
*Function:实现变长参数构造树&遍历树函数&错误处理函数,yyparse()启动文法分析
*/
# include
# include
# include//变长参数函数所需的头文件
# include"gramtree_v1.h"
int i;
struct ast *newast(char* name,int num,...)//抽象语法树建立
{
va_list valist; //定义变长参数列表
struct ast *a=(struct ast*)malloc(sizeof(struct ast));//新生成的父节点
struct ast *temp=(struct ast*)malloc(sizeof(struct ast));
if(!a)
{
yyerror("out of space");
exit(0);
}
a->name=name;//语法单元名字
va_start(valist,num);//初始化变长参数为num后的参数
if(num>0)//num>0为非终结符:变长参数均为语法树结点,孩子兄弟表示法
{
temp=va_arg(valist, struct ast*);//取变长参数列表中的第一个结点设为a的左孩子
a->l=temp;
a->line=temp->line;//父节点a的行号等于左孩子的行号
if(num>=2) //可以规约到a的语法单元>=2
{
for(i=0; i1; ++i)//取变长参数列表中的剩余结点,依次设置成兄弟结点
{
temp->r=va_arg(valist,struct ast*);
temp=temp->r;
}
}
}
else //num==0为终结符或产生空的语法单元:第1个变长参数表示行号,产生空的语法单元行号为-1。
{
int t=va_arg(valist, int); //取第1个变长参数
a->line=t;
if((!strcmp(a->name,"ID"))||(!strcmp(a->name,"TYPE")))//"ID,TYPE,INTEGER,借助union保存yytext的值
{char*t;t=(char*)malloc(sizeof(char* )*40);strcpy(t,yytext);a->idtype=t;}
else if(!strcmp(a->name,"INTEGER")) {a->intgr=atoi(yytext);}
else {}
}
return a;
}
void eval(struct ast *a,int level)//先序遍历抽象语法树
{
if(a!=NULL)
{
for(i=0; i//孩子结点相对父节点缩进2个空格
printf(" ");
if(a->line!=-1){ //产生空的语法单元不需要打印信息
printf("%s ",a->name);//打印语法单元名字,ID/TYPE/INTEGER要打印yytext的值
if((!strcmp(a->name,"ID"))||(!strcmp(a->name,"TYPE")))printf(":%s ",a->idtype);
else if(!strcmp(a->name,"INTEGER"))printf(":%d",a->intgr);
else
printf("(%d)",a->line);
}
printf("\n");
eval(a->l,level+1);//遍历左子树
eval(a->r,level);//遍历右子树
}
}
void yyerror(char*s,...) //变长参数错误处理函数
{
va_list ap;
va_start(ap,s);
fprintf(stderr,"%d:error:",yylineno);//错误行号
vfprintf(stderr,s,ap);
fprintf(stderr,"\n");
}
int main()
{
printf(">");
return yyparse(); //启动文法分析,调用词法分析
}
当yyparse()启动文法分析时,bison文法分析调用flex词法分析,在flex中,扫描到一个词法单元时,由newast(…)建立叶子结点,由return返回该词法单元到bison中。
词法单元属于终结符,在语法树中是叶子结点。
调用newast函数时仅仅生成一个新的结点,不做孩子兄弟表示法的相关工作。传递给newast的参数有3个,第1个是词法单元的名字,第2个是0,表示这是终结符,建立的是叶子节点,后续没有孩子结点。第3个参数是yylineno,是词法单元所在的行号,所有由该词法单元规约得到的语法单元的行号,都和该词法单元相同。
/*
*Name:gramtree.l
*Author:WangLin
*Created on:2015-10-03
*Function:词法分析模块,对每个终结符建立一个叶子结点,返回记号,供bison语法分析使用
*/
%{
#include "stdio.h"
#include "stdlib.h"
# include "gramtree_v1.h"
#include "gramtree_v1.tab.h"
%}
%option yylineno
TYPE int|float
STRUCT struct
RETURN return
IF if
ELSE else
WHILE while
PLUS \+
MINUS -
INTEGER [1-9]+[0-9]*|0
FLOAT [0-9]+\.[0-9]*
ID [a-z_A-Z][a-zA-Z_0-9]*
SPACE [ \t\r]*
EOL \n
SEMI ;
COMMA ,
ASSIGNOP =
RELOP >|<|>=|<=|==|!=
STAR \*
DIV \/
AND &&
OR \|\|
DOT \.
NOT !
LP \(
RP \)
LB \[
RB \]
LC \{
RC \}
AERROR .
%%
int|float {yylval.a=newast("TYPE",0,yylineno);return TYPE;}
struct {yylval.a=newast("STRUCT",0,yylineno);return STRUCT;}
{RETURN} {yylval.a=newast("RETURN",0,yylineno); return RETURN;}
{IF} { yylval.a=newast("IF",0,yylineno);return IF;}
{ELSE} {yylval.a=newast("ELSE",0,yylineno); return ELSE;}
{WHILE} {yylval.a=newast("WHILE",0,yylineno); return WHILE;}
{PLUS} {yylval.a=newast("PLUS",0,yylineno); return PLUS;}
{MINUS} {yylval.a=newast("MINUS",0,yylineno); return MINUS;}
{INTEGER} {yylval.a=newast("INTEGER",0,yylineno); return INTEGER;}
{ID} {yylval.a=newast("ID",0,yylineno); return ID;}
{SPACE} {}
{EOL} {}
{SEMI} {yylval.a=newast("SEMI",0,yylineno); return SEMI;}
{COMMA} {yylval.a=newast("COMMA",0,yylineno); return COMMA;}
{ASSIGNOP} {yylval.a=newast("ASSIGNOP",0,yylineno); return ASSIGNOP;}
{RELOP} {yylval.a=newast("RELOP",0,yylineno); return RELOP;}
{STAR} {yylval.a=newast("STAR",0,yylineno); return STAR;}
{DIV} {yylval.a=newast("DIV",0,yylineno); return DIV;}
{AND} {yylval.a=newast("AND",0,yylineno); return AND;}
{OR} {yylval.a=newast("OR",0,yylineno); return OR;}
{DOT} {yylval.a=newast("DOT",0,yylineno); return DOT;}
{NOT} {yylval.a=newast("NOT",0,yylineno); return NOT;}
{LP} {yylval.a=newast("LP",0,yylineno); return LP;}
{RP} {yylval.a=newast("RP",0,yylineno); return RP;}
{LB} {yylval.a=newast("LB",0,yylineno); return LB;}
{RB} {yylval.a=newast("RB",0,yylineno); return RB;}
{LC} {yylval.a=newast("LC",0,yylineno); return LC;}
{RC} {yylval.a=newast("RC",0,yylineno); return RC;}
{AERROR} { printf("Error type A at line %d: Mystirious charachter '%s'\n",yylineno,yytext);}
%%
int yywrap()
{
return 1;
}
(1)在该文件中定义词法记号,flex返回的词法单元将在bison语法分析过程中使用;
(2)定义运算符的优先级和结合性,如果不指明,bison将会产生移进和规约冲突
conflicts: 1 shift/reduce ,指明优先级和结合性可以消除冲突。
(3)树的建立顺序是自底向上的,flex中先给词法单元建立了对应的结点,在语法树中属于叶子结点,在bison中,由词法单元对应的叶子结点规约出新的父节点,又由新的父节点和其他结点规约到新的父节点,自底向上建立语法树,如果规约到起始语法单元program,就表示整棵语法树都被建立好了。
树的建立是按照规则去规约的,对于要进行规约每一条规则,规则的左部是要新生成的父节点,规则的右部的每个语法单元对应的结点都是已经建立好的,都是左部的孩子结点,在孩子兄弟表示法中,将规则右部的第一个结点设为父节点的左孩子,第2个结点是第1个结点的右孩子,即右兄弟,第3个是第2个的右兄弟,依次…直到规则右部的最后一个语法单元。
(4)调用newast建立新的结点时,第1个参数是规则左部文法单元的名字,第2个参数表示规则右部的文法单元的数目,变长参数部分依次就是规则右部的各个文法单元。调用之后,就代表由规则右部的语法单元规约到了规则左部的语法单元。
语法树建立完成之后,在起始符号program的action中打印语法树。
/*
*Name:gramtree_v1.y
*Author:WangLin
*Created on:2015-10-03
*Function:bison语法分析,对每条规则 按照孩子兄弟表示法建立语法结点
*/
%{
#include
#include
#include "gramtree_v1.h"
%}
%union{
struct ast* a;
double d;
}
/*declare tokens*/
%token INTEGER FLOAT
%token TYPE STRUCT RETURN IF ELSE WHILE ID SPACE SEMI COMMA ASSIGNOP RELOP PLUS
MINUS STAR DIV AND OR DOT NOT LP RP LB RB LC RC AERROR
%token EOL
%type Program ExtDefList ExtDef ExtDecList Specifire StructSpecifire
OptTag Tag VarDec FunDec VarList ParamDec Compst StmtList Stmt DefList Def DecList Dec Exp Args
/*priority*/
%right ASSIGNOP
%left OR
%left AND
%left RELOP
%left PLUS MINUS
%left STAR DIV
%right NOT
%left LP RP LB RB DOT
%%
Program:|ExtDefList {$$=newast("Program",1,$1);printf("打印syntax tree:\n");eval($$,0);printf("syntax tree打印完毕!\n\n");}
;
ExtDefList:ExtDef ExtDefList {$$=newast("ExtDefList",2,$1,$2);}
| {$$=newast("ExtDefList",0,-1);}
;
ExtDef:Specifire ExtDecList SEMI {$$=newast("ExtDef",3,$1,$2,$3);}
|Specifire SEMI {$$=newast("ExtDef",2,$1,$2);}
|Specifire FunDec Compst {$$=newast("ExtDef",3,$1,$2,$3);}
;
ExtDecList:VarDec {$$=newast("ExtDecList",1,$1);}
|VarDec COMMA ExtDecList {$$=newast("ExtDecList",3,$1,$2,$3);}
;
/*Specifire*/
Specifire:TYPE {$$=newast("Specifire",1,$1);}
|StructSpecifire {$$=newast("Specifire",1,$1);}
;
StructSpecifire:STRUCT OptTag LC DefList RC {$$=newast("StructSpecifire",5,$1,$2,$3,$4,$5);}
|STRUCT Tag {$$=newast("StructSpecifire",2,$1,$2);}
;
OptTag:ID {$$=newast("OptTag",1,$1);}
|{$$=newast("OptTag",0,-1);}
;
Tag:ID {$$=newast("Tag",1,$1);}
;
/*Declarators*/
VarDec:ID {$$=newast("VarDec",1,$1);}
| VarDec LB INTEGER RB {$$=newast("VarDec",4,$1,$2,$3,$4);}
;
FunDec:ID LP VarList RP {$$=newast("FunDec",4,$1,$2,$3,$4);}
|ID LP RP {$$=newast("FunDec",3,$1,$2,$3);}
;
VarList:ParamDec COMMA VarList {$$=newast("VarList",3,$1,$2,$3);}
|ParamDec {$$=newast("VarList",1,$1);}
;
ParamDec:Specifire VarDec {$$=newast("ParamDec",2,$1,$2);}
;
/*Statement*/
Compst:LC DefList StmtList RC {$$=newast("Compst",4,$1,$2,$3,$4);}
;
StmtList:Stmt StmtList{$$=newast("StmtList",2,$1,$2);}
| {$$=newast("StmtList",0,-1);}
;
Stmt:Exp SEMI {$$=newast("Stmt",2,$1,$2);}
|Compst {$$=newast("Stmt",1,$1);}
|RETURN Exp SEMI {$$=newast("Stmt",3,$1,$2,$3);}
|IF LP Exp RP Stmt {$$=newast("Stmt",5,$1,$2,$3,$4,$5);}
|IF LP Exp RP Stmt ELSE Stmt {$$=newast("Stmt",7,$1,$2,$3,$4,$5,$6,$7);}
|WHILE LP Exp RP Stmt {$$=newast("Stmt",5,$1,$2,$3,$4,$5);}
;
/*Local Definitions*/
DefList:Def DefList{$$=newast("DefList",2,$1,$2);}
| {$$=newast("DefList",0,-1);}
;
Def:Specifire DecList SEMI {$$=newast("Def",3,$1,$2,$3);}
;
DecList:Dec {$$=newast("DecList",1,$1);}
|Dec COMMA DecList {$$=newast("DecList",3,$1,$2,$3);}
;
Dec:VarDec {$$=newast("Dec",1,$1);}
|VarDec ASSIGNOP Exp {$$=newast("Dec",3,$1,$2,$3);}
;
/*Expressions*/
Exp:Exp ASSIGNOP Exp{$$=newast("Exp",3,$1,$2,$3);}
|Exp AND Exp{$$=newast("Exp",3,$1,$2,$3);}
|Exp OR Exp{$$=newast("Exp",3,$1,$2,$3);}
|Exp RELOP Exp{$$=newast("Exp",3,$1,$2,$3);}
|Exp PLUS Exp{$$=newast("Exp",3,$1,$2,$3);}
|Exp MINUS Exp{$$=newast("Exp",3,$1,$2,$3);}
|Exp STAR Exp{$$=newast("Exp",3,$1,$2,$3);}
|Exp DIV Exp{$$=newast("Exp",3,$1,$2,$3);}
|LP Exp RP{$$=newast("Exp",3,$1,$2,$3);}
|MINUS Exp {$$=newast("Exp",2,$1,$2);}
|NOT Exp {$$=newast("Exp",2,$1,$2);}
|ID LP Args RP {$$=newast("Exp",4,$1,$2,$3,$4);}
|ID LP RP {$$=newast("Exp",3,$1,$2,$3);}
|Exp LB Exp RB {$$=newast("Exp",4,$1,$2,$3,$4);}
|Exp DOT ID {$$=newast("Exp",3,$1,$2,$3);}
|ID {$$=newast("Exp",1,$1);}
|INTEGER {$$=newast("Exp",1,$1);}
|FLOAT{$$=newast("Exp",1,$1);}
;
Args:Exp COMMA Args {$$=newast("Args",3,$1,$2,$3);}
|Exp {$$=newast("Args",1,$1);}
;
%%
编写要进行分析的代码段,举例
vim input3.c
int inc()
{
int i=1;
float j;
return 0;
}
运行,输出结果:
[root@localhost flex]# cat input3.c|./a.out
>打印syntax tree:
Program (1)
ExtDefList (1)
ExtDef (1)
Specifire (1)
TYPE :int
FunDec (1)
ID :inc
LP (1)
RP (1)
Compst (2)
LC (2)
DefList (3)
Def (3)
Specifire (3)
TYPE :int
DecList (3)
Dec (3)
VarDec (3)
ID :i
ASSIGNOP (3)
Exp (3)
INTEGER :1
SEMI (3)
DefList (4)
Def (4)
Specifire (4)
TYPE :float
DecList (4)
Dec (4)
VarDec (4)
ID :j
SEMI (4)
StmtList (5)
Stmt (5)
RETURN (5)
Exp (5)
INTEGER :0
SEMI (5)
RC (6)
syntax tree打印完毕!
[root@localhost flex]#
结果正确输出了语法树,输出语法单元的名字和行号,行号和规约到该语法 单元的词法单元行号一直,对于产生空的语法单元,只输出空行,不输出语法结点的其他信息;
对于ID,TYPE,INTEGER等词法单元,输出了词法单元的值。
这次实验碰到了许多问题,前后花了好几天的时间,做完后感觉收获颇多。
char* p1="Linwhite";
char* p2;
p2=p1;
p1初始化时分配了内存空间,p2=p1将p2指向p1指向的内存空间,即p1和p2指向同一内存。
yytext是一个字符指针,它的值是当前扫描到的词。词法分析开始时,yytext指向一块内存(缓冲区)的首地址,每扫描到一个词,就将这个词写入到缓冲区中,随后yytext指向这块缓冲区连续的下一个内存地址。假设,定义一个字符指针s:
char* s=yytext;
扫描开始时,将s指向了yytext指向的缓冲区的首地址,这个地址是不会变的,打印s时,将从缓冲区首地址开始把缓冲区内所有内容都打印出来,由于yytext指向的地址是连续变化的,也就是说缓冲区的内容也在不断增加,s指向的内容也在不断增加,词法扫描结束后,s指向的就是整个代码段。
假设以下是要进行语法分析的代码段:
int inc()
{
int i=1;
float j;
return 0;
}
启动yyparse(),开始文法分析,文法分析调用词法分析,开始时yytext的第一个值是字符串“int“。
此时执行:
printf("%s",s);
输出:int
因为此时缓冲区的内容只有yytext扫描到的第一个词”int“,而s指向的是这块缓冲区的首地址,也就是说缓冲区里只有”int“。
当整个程序的词全部扫描完毕,整个代码段中的词全部都被写入到缓冲区中了,而此时s指向的是这块缓冲区的首地址,此时再执行:
printf("%s",s);
会将全部代码段输出:
int inc()
{
int i=1;
float j;
return 0;
}
本质的问题是字符串指针的内存分配。为了保证整个代码块词法扫描结束后,s的值是某一时刻yytext扫描到的值,而不是整个缓冲区的内容,就要使s不要和yytext指向同一内存,可以用malloc解决:
char* s;
s=(char* )malloc(sizeof(char*)*20);
strcpy(s,yytext);
char* s;仅仅是定义了s,程序不会给s分配内存。malloc给s分配了内存空间,这块内存空间和yytext指向的内存空间是不相同的,strcpy将yytext的值拷贝到了s指向的内存中,内存中的值不会变化,无论你何时打印s的值,都是你初始拷贝进去的值,除非你改变它。
struct ast *newast(char* name,int num,...)
(1)char* name是固定参数,num表示变长参数的个数
(2)定义变长参数列表
va_list valist; //定义变长参数列表
(3)初始化变长参数
va_start(valist,num);//初始化变长参数为num后的参数
(4)va_arg(valist, type)
valist代表之前定义的参数列表的名字
type代表实际传进来的变长参数的类型
执行第一次就是取第1个变长参数,然后指针指向第2个变长参数,执行第2次时就是取第2个,依次往后。
传进来的变长参数的类型没有限制,只要在取参数时type设置成和传进来的一样即可,变长参数之间的类型也不需要保持一致。
temp=va_arg(valist, struct ast*);//取变长参数列表中的参数
union//共用体
{
char* idtype;
int intgr;
float flt;
};
共用体中所有成员共享一块内存;共用体的大小 由成员中占据最大的那个空间大小决定,上面这个union的大小就等于float所占的内存大小,因为float占用的空间比char*和int都要大,而这3个成员是共享内存的。