flex&bison编写语法分析器

使用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的知识点可以自行百度。

下面是第一种编译方法的具体过程:

【实验程序】

0,Makefile——编译文件

程序总共包含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中的代码进行语法分析,打印出语法树。

gramtree_v1.h—定义创建和遍历语法树函数的头文件

/* *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);

gramtree_v1.c——实现创建和遍历语法树函数

/* *Name:gramtree_v1.c *Author:WangLin *Created on:2015-10-03 *Function:实现变长参数构造树&遍历树函数&错误处理函数,yyparse()启动文法分析 */
# include<stdio.h>
# include<stdlib.h>
# include<stdarg.h>//变长参数函数所需的头文件
# 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; i<num-1; ++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<level; ++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(); //启动文法分析,调用词法分析
}

gramtree.l代码——flex词法分析模块

当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;
}

gramtree_v1.y代码——bison语法分析模块

(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<unistd.h>
#include<stdio.h>   
#include "gramtree_v1.h"
%}

%union{
struct ast* a;
double d;
}
/*declare tokens*/
%token  <a> INTEGER FLOAT
%token <a> 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 <a> EOL
%type  <a> 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等词法单元,输出了词法单元的值。

问题总结

这次实验碰到了许多问题,前后花了好几天的时间,做完后感觉收获颇多。

  • 总结1:字符串指针的内存分配
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的值,都是你初始拷贝进去的值,除非你改变它。

  • 总结2:变长参数函数的使用
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*);//取变长参数列表中的参数
  • 总结3:union的使用
 union//共用体
    {
    char* idtype;
    int intgr;
    float flt;
    };

共用体中所有成员共享一块内存;共用体的大小 由成员中占据最大的那个空间大小决定,上面这个union的大小就等于float所占的内存大小,因为float占用的空间比char*和int都要大,而这3个成员是共享内存的。

  • 总结4:小感悟
    写完这个程序收获的东西很多,一方面对于flex和bison语法分析的原理有了深刻的体会,另一方面c语言的编程能力又得到了提高,像union和变长参数都是之前完全没有接触过的东西。总的来说,程序相对较难,但是写出来之后收获很大,(写出来之后就觉得程序很简单了。。。)
    代码还有很多需要优化或者增加的地方,如果想继续深挖的话,还有许多可做的工作。

你可能感兴趣的:(UNION,Flex,编译原理,变长参数,bison)