使用lex和yacc识别程序中的括号

简介

  针对的是程序中出现的括号,包括大括号、中括号和括号,希望程序能够返回输入文件中括号的位置和嵌套深度。
  程序中允许出现除了’@‘之外其它所有符号,所以在java中,’@override’假设是不会出现的,用‘@’作为结束符号,而不是’$’,因为R语言里面列选择符号是’$’。输入的语言可以是C/C++、python、java、R、JavaScript、PHP,因为测试时使用的代码有限,测试时暂未发现除了有些地方出现中文,会出现奇怪的错误之外的问题。

文法定义

G[S]:
  S→S { S }
  S→S [ S ]
  S→S ( S )
  S→ε

单词定义

名称在程序中出现形式
左大括号LD{
右大括号RD}
左中括号LZ[
右中括号RZ]
左小括号LS(
右小括号RS)

允许出现的符号串

名称在程序中出现形式
括号语言中的单词{ } [ ] ( )
换行符\n
表示空白的符号\t和空格
特殊符号_$#+-*\=<>!|?%&;:,./
字母、数字0-9及a-z和A-Z
双引号内容 包围的串,可以跨行,串中没有双引号
单引号内容 包围的串,可以跨行,串中没有单引号
双斜杠注释// 开始的注释,不能跨行
跨行注释由/* */包围的串
井号注释# 开始的单行注释
html标记由<>和</>形成的串
结束符号@

词法分析单元

设计思路

   首先需要确定作为输入的程序文件中允许出现哪些单词符号,重点关注的是哪些符号。以及哪些部分将来需要送到语法分析单元中,哪些部分是识别出来不需要送回给语法单元的。
  识别单词,可以使用DFA,使用lex工具,需要交给lex的是lex支持的正则表达式。所以识别单词,需要正确地分别对每种单词串构造正确的正则表达式。然后对每种单词串定义相应的动作,如果是换行符、空白、other等,动作是打印出属性;如果是左括号,例如左大括号,需要记录它的行号和列号,并对大括号的嵌套深度变量进行加1操作,返回给语法分析器的是在语法分析器中定义的语法成分LD;如果是右大括号,需要记录它的行号和列号,并对大括号的嵌套深度变量进行减1操作,返回给语法分析器的是在语法分析器中定义的语法成分RD;如果读到结束符号’@’,需要返回给语法分析器的是在语法分析器中定义的语法成分END。
  词法分析器向语法分析器传递消息,除了return语法成分之外,还可以使用lex和yacc支持的共享变量yylval,使用它来传递行号、列号和嵌套深度信息。嵌套的深度变量可以在语法分析器中定义,由语法分析器和词法分析器共享。
  为了获得行号,使用lex提供的yylineno变量,它能够返回当前识别的括号所处的行号。在lex中,yylineno的值默认是1,为了它能够返回正确的行号,需要在lex中定义“%option yylineno”。
  为了获得列号,使用lex提供的YY_USER_ACTION宏,对它重新定义,定义为一个函数,每次在识别成分之前会调用该函数,获得yylloc结构信息,包括first_line、first_column、last_line、last_column。这些信息在之后语法分析错误处理的时候也可以用上,用来定位出错位置。

数据结构

    使用了lex和yacc提供的用来定位的结构yylloc,yylloc的类型是YYLTYPE,它有first_line、first_column、last_line、last_column四个成员。
    在yacc文件中重新定义了yylval的类型,它有嵌套深度depth,当前行号nowline和当前列号nowcol三个成员。

typedef struct YYLTYPE YYLTYPE;
struct YYLTYPE
{
  int first_line;
  int first_column;
  int last_line;
  int last_column;
};
typedef struct myType
{
	int depth;
	int nowline;
	int nowcol;
}myType;

#define YYSTYPE myType

关键代码

  1. 记录行号和列号部分
extern YYLTYPE yylloc; /* 用于定位的,在yacc中提供,需要声明为extern*/
static void update_loc() /* 用于定位的行列的,每次在识别一个成分之前会调用*/
{
  static int curr_line = 1;/*静态变量*/
  static int curr_col  = 1;
  yylloc.first_line   = curr_line;
  yylloc.first_column = curr_col;
  {
      char * s; 
	  for(s = yytext; *s != '\0'; s++) /*yytext是取到词的数组的开始地址*/
	   {
		if(*s == '\n'){/*是换行符行数+1*/
		  curr_line++;
		  curr_col = 1;
		}
		else{
		  curr_col++;
		}
	  }
  }
  yylloc.last_line   = curr_line;
  yylloc.last_column = curr_col-1;
}

#define YY_USER_ACTION update_loc();  /*通过改写这个lex提供的宏,来实现定位*/

  1. 正则表达式部分
/*交给lex的正则表达式*/
%option yylineno   /*lex提供的行号,需要使用option声明,否则会一直为1*/
delim   [ \t]       /*空白符包括空格和缩进符*/
ws      {delim}+  /*空白由连续的1或多个空白符组成*/

character   [a-zA-Z0-9]                /*字母和数字*/
operator  [_\$#+\-\*\/=<>!\\|?%&;:\,\.\\]   /*特殊符号,不包括括号和引号和@*/
other   ({character}|{operator})+    /*特殊符号和字母连续交替出现,不是关心的成分*/
dbps	"/"\*(.|\n)*\*"/"     /*跨行注释,所以是(.|/n),需要包括换行符*/
sgps	\/\/.*            /*单行双斜杠注释,不用包括换行符*/
spps	#.*             /*井号单行注释,不用包括换行符*/
dbquo   \"[^"]*\"       /*双引号中的内容,双引号中不能有引号,否则这是双引号配对的问题不是关心的*/
sgquo	'[^']*'           /*单引号中的内容,中间不会有单引号*/
htmlps	\<[^\>]+\>[^\<\>]*\<\/[^\>]+\>  /*html中标记,词法分析中写了,后来没有用到*/
qandp   {dbquo}|{sgquo}|{dbps}|{sgps}|{spps}|{htmlps}  /*注释和引号中的内容*/
  1. 每个单词对应的动作
\n	    {}   /*换行符*/
{ws}       {}    /*空白*/
{qandp}    {}  /*注释和引号内容*/
{other}     {}  /*其它非关注内容*/

/*左小括号的动作是,左括号嵌套深度+1,左括号的嵌套深度、行号、列号通过yylval变量传递给语法分析器,return(LS)表示识别到的是左小括号*/
"("     {Sdepth++;yylval.depth=Sdepth;yylval.nowline=yylineno;
yylval.nowcol=yylloc.last_column;return(LS);}

/*右小括号的动作是,括号嵌套深度为当前小括号嵌套深度,再对嵌套深度变量做减1操作,左括号的嵌套深度、行号、列号通过yylval变量传递给语法分析器,return(LS)表示识别到的是左小括号*/
")"	{yylval.depth=Sdepth;Sdepth--;yylval.nowline=yylineno;
yylval.nowcol=yylloc.last_column;return(RS);}

"["	{Zdepth++;yylval.depth=Zdepth;yylval.nowline=yylineno;
yylval.nowcol=yylloc.last_column;return(LZ);}

"]"     {yylval.depth=Zdepth;Zdepth--;yylval.nowline=yylineno;
yylval.nowcol=yylloc.last_column;return(RZ);}

"{"	{Ddepth++;yylval.depth=Ddepth;yylval.nowline=yylineno;
yylval.nowcol=yylloc.last_column;return(LD);}

"}"     {yylval.depth=Ddepth;Ddepth--;yylval.nowline=yylineno;
yylval.nowcol=yylloc.last_column;return(RD);}

"@"      {return(END);}  /*结束符号*/

流程图

在这里插入图片描述

语法、语义分析

设计思路

  使用yacc工具,它的方法是LR分析方法,需要传递给yacc的是语法规则。因为yacc使用的是LR分析方法,所以文法可以是左递归的。文法如果是右递归的,当输入文件语法正确时能够正确处理,但是一旦输入文件中有语法错误,括号不匹配时,如{}}{}@,在识别到第二个右括号时,如果想跳过这个错误继续往下识别,就比较复杂。如果使用左递归的文法,出错处理可行性更高。
  文法中,可以借助M→ε这样的产生式来完成一些动作,一开始也是这么做的。后来修改程序之后,文法中产生式改为S→S A S B,以及A→(,B→),这样的文法,也能够起到和空产生式一样的添加动作记录信息的效果。
  中间代码形式为四元组(括号类型,嵌套深度,行号:列号)。为了生成这样的中间代码,产生式为:
S→S A S B
  | S C S D
  | S E S F
  | ε
A→LD /* 左大括号 */
B→RD /* 右大括号 */
C→LZ /* 左中括号 */
D→RZ /* 右中括号 */
E→LS /* 左小括号 */
F→RS /* 右小括号 */
  语义规则的制定,是由中间代码的形式决定的。所以LD规约到A以后,执行的动作也就是A→LD的语义规则是,前一个括号类型=现在的括号类型,现在的括号类型为yytext[0]中读出的括号类型。A的嵌套深度值=LD的嵌套深度值,A的行号列号=LD的行号列号。其它情况与此类似。
  当S A S B规约到S时,S→S A S B的语义规则是,打印出“{”和它的嵌套深度、行号列号,打印出与它配对的“}”的嵌套深度、行号列号。其它情况与此类似。
  以上部分,能够解决程序语法正确的情况。万一程序中括号不匹配,就需要报错,而且最好能有出错的行号列号信息和出错的括号类型。并且如果只错了一个或两个,最好能接着往后扫描。为了实现出错处理,需要对以上文法进行修改。利用yacc中定义了的error,增加一些产生式S→error G,G→A|B|C|D|E|F。当出现个别错误的时候,yacc就用这条产生式规约,执行自定义的错误处理函数,并继续往下执行。

数据结构和关键代码

定义的符号,lex用做返回值:

%token LS   /*左小括号*/
%token RS   /*右小括号*/
%token LZ   /*左中括号*/
%token RZ   /*右中括号*/
%token LD   /*左大括号*/
%token RD   /*右大括号*/
%token END  /*结束符号*/

用于错误处理的数据结构:

struct err
{
	int totalerrors; /*总共发生了多少个错误*/
	int lineinfo[3]; /*错误的行号信息*/
	int colinfo[3];  /*错误的列号信息*/
	char errorchar[3];/*错误符号*/
	int state[3]; /*如果是多了一个符号,就是1*/	
};

流程图在这里插入图片描述

代码

%{

#include <stdio.h> 
#include <stdlib.h>
#include <string.h>

extern char *yytext; /* yytext是取到词的数组的开始地址*/
extern int yylineno; 

extern FILE *yyin; /*输入文件的指针*/
FILE *yyout1; /*词法分析的输出文件指针*/
FILE *yyout2; /*语义分析及中间代码生成的输出文件的指针*/

char lastptr=' ';//last char
char nowptr=' ';
int totalwords=0;
int curline=1;/* get the current line number*/
int Ddepth=0;/*大括号的嵌套深度 */
int Zdepth=0;/* 中括号的嵌套深度 */
int Sdepth=0;/*小括号的嵌套深度*/


struct err /*出错处的结构体*/
{
	int totalerrors; /*目前为止已经发现的错误个数 */
	int lineinfo[3]; /*每个错误出现的行数 */
	int colinfo[3]; /*每个错误出现的列数 */
	char errorchar[3];/*出错字符*/
	int state[3]; /*如果是多打了一个括号这样的错误,state=1 */	

};

struct err errorinfo={0,{0,0,0},{1,1,1},{'#','#','#'},{0,0,0}}; /*出错信息初始化*/

/*yylval的类型,有嵌套深度depth,当前行号nowline和当前列号nowcol三个成员*/
typedef struct myType 
{
	int depth;
	int nowline;
	int nowcol;
}myType;

#define YYSTYPE myType 


void myerror(int line,int col);
void summary();


%}
/*记号声明*/
%token LS
%token RS
%token LZ
%token RZ
%token LD
%token RD
%token END
%token OTH

/*yacc规则段:yacc文件的主体,包括每个产生式是如何匹配的,以及匹配后要执行的C代码动作。*/
%%  
begin	: S END		   {summary();fclose(yyout2);} /*处理完整个文件,汇总出错信息输出,关闭result2.txt*/
	;
/*将左右括号的行号列号嵌套深度写入输出文件*/
S	:S  A   S  B      
	{fprintf(yyout2,"{ %d at %d:%d\n",$2.depth,$2.nowline,$2.nowcol);fprintf(yyout2,"} %d at %d:%d\n",$4.depth,$4.nowline,$4.nowcol);} 
	|S  C   S  D      
	{fprintf(yyout2,"[ %d at %d:%d\n",$2.depth,$2.nowline,$2.nowcol);fprintf(yyout2,"] %d at %d:%d\n",$4.depth,$4.nowline,$4.nowcol);}
	|S  E   S  F      
	{fprintf(yyout2,"( %d at %d:%d\n",$2.depth,$2.nowline,$2.nowcol);fprintf(yyout2,") %d at %d:%d\n",$4.depth,$4.nowline,$4.nowcol);}
	| 		   {}
	|error  G	   {myerror(@1.last_line,@1.last_column);}/*出错处理*/
	;

G	:A|B|C|D|E|F;

/*读到括号进行归约,将lastptr移动到当前字符的位置,将行号列号嵌套深度的信息传递给产生式左侧*/	   
A	: LD
{lastptr=nowptr;nowptr=yytext[0];$$.depth=$1.depth;$$.nowline=$1.nowline;$$.nowcol=$1.nowcol;}
	;

B	: RD		   {lastptr=nowptr;nowptr=yytext[0];$$.depth=$1.depth;$$.nowline=$1.nowline;$$.nowcol=$1.nowcol;}
	;


C	: LZ		   {lastptr=nowptr;nowptr=yytext[0];$$.depth=$1.depth;$$.nowline=$1.nowline;$$.nowcol=$1.nowcol;}
	;

D	: RZ		   {lastptr=nowptr;nowptr=yytext[0];$$.depth=$1.depth;$$.nowline=$1.nowline;$$.nowcol=$1.nowcol;}
	;


E	: LS		   {lastptr=nowptr;nowptr=yytext[0];$$.depth=$1.depth;$$.nowline=$1.nowline;$$.nowcol=$1.nowcol;}
	;

F	: RS		   {lastptr=nowptr;nowptr=yytext[0];$$.depth=$1.depth;$$.nowline=$1.nowline;$$.nowcol=$1.nowcol;}
	;
/*C函数定义段:如出错处理,汇总输出结果,输入文件类型的判断等一些函数的定义*/
%%
#include "lex.yy.c"
void myerror(int line,int col) /*出错处理程序,小于三个错统计出错处的行数列数和类型,大于三个错直接终止分析程序*/
{

	char errchar;
	if(nowptr!=' ')
		errchar=nowptr;
	else 
		errchar='#';

	if(errchar=='}')	
		Ddepth++;
	else if(errchar==']')
		Zdepth++;
	else
		Sdepth++;
	errorinfo.totalerrors++;
	if(errorinfo.totalerrors<3)
	{
		errorinfo.state[errorinfo.totalerrors-1]=1;
		errorinfo.errorchar[errorinfo.totalerrors-1]=errchar;
		errorinfo.lineinfo[errorinfo.totalerrors-1]=line;
		errorinfo.colinfo[errorinfo.totalerrors-1]=col;
	}
	else if(errorinfo.totalerrors>=3)
	{
		printf("Too many syntax errors!Please check carefully!\n");
		exit(1);	
	}

}

void summary() /*汇总出错信息并进行输出*/
{
	if(errorinfo.totalerrors==0)
		printf("Syntax check OK!\n");
	else
	{
		printf("You have %d errors(s),please check your syntax\n",errorinfo.totalerrors);
		for(int i =0;i<errorinfo.totalerrors;i++)/*输出每个出错处的行号列号以及出错字符*/
		{
				if(errorinfo.state[i]==1&&errorinfo.errorchar[i])
				{
				 printf("Error: in line %d:%d,",errorinfo.lineinfo[i],errorinfo.colinfo[i]);
				 printf("Maybe extra ' %c '\n",errorinfo.errorchar[i]);
				}
		}
	}
}

void yyerror(char *s) /*出错处提示*/
{
   printf("syntax error!\n");
}


void get_name(const char *file_name,char *extension) /*输入文件处理程序,判断程序的类型,根据输入文件的命名来判断*/
{
    int i=0,length;
    length=strlen(file_name);
        while(file_name[i])
    {
        if(file_name[i]=='.')
        	break;
	i++;
    }
    if(i<length)
{
    strncpy(extension,file_name,i+1);
    extension[i]='\0';
}
    else
    strcpy(extension,"\0");
}


int main(int argc,char *argv[])
{
	
    char b[10];
    char ch;
    if(argc==1) /*未输入文件参数*/
    {
        printf("have not enter file name strike any key exit");
        exit(0);
    }

    
    printf("input file is %s\n",argv[1]);

    get_name(argv[1],b);
    printf("type of input file is %s\n",b);

	
    if((yyin=fopen(argv[1],"rt"))==NULL) /*输入文件是否打开正确*/
   {
       printf("Cannot open %s\n",argv[1]);
  
       exit(1);
   }
	
   if(argc==2) 
	yyout=stdout;

   else if((yyout1=fopen(argv[2],"wt+"))==NULL) /*词法分析结果的输出文件是否打开正确*/
   {
       printf("Cannot open %s\n",argv[2]);
       exit(1);
   }
   

   else if((yyout2=fopen(argv[3],"wt+"))==NULL) /*语义分析及中间代码生成的输出文件是否打开正确*/
   {
       printf("Cannot open %s\n",argv[3]);
       exit(1);
   }
   printf("output file is %s\n",argv[2]);
   printf("output file is %s\n",argv[3]);

    yyparse();  /*进行语法语义分析及中间代码生成*/
    fclose(yyin); /*关闭输入文件*/
    return 0;
}

补充说明

  lex和yacc的代码如果分开写在mylex.l和yacc.y中,编译时,输入如下命令,得到可执行文件tst,在当前目录输入./tst<test.cpp就能把test.cpp作为tst的输入,得到效果,把上面语法语义分析部分的读写文件操作改成printf即可:
lex mylex.l
yacc yacc.y
cc y.tab.c -ly -ll -o tst
  当时在写这个的时候,找了很多很多资料,很多地方大佬们没有说明白为啥是这么做的,在Stack Overflow都一时半会儿翻不到答案,比如为什么yylino会一直是1,这些是在yacc和lex的文档里面才看得到的那些编译选项的说明。
  有的地方比如错误定位,可以查yylloc或者tracking作为关键词,有一个IBM的写得非常详细,看完他的博客然后再去翻Bison的文档,会容易理解很多;如果一时半会儿不能理解可以在Stack Overflow上找到对应的回答,比较靠近底部的就是本博文选择的方法,定义了一个函数来更新行号列号。
  很多地方并不是很完善。

  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值