文章目录
简介
针对的是程序中出现的括号,包括大括号、中括号和括号,希望程序能够返回输入文件中括号的位置和嵌套深度。
程序中允许出现除了’@‘之外其它所有符号,所以在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
关键代码
- 记录行号和列号部分
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提供的宏,来实现定位*/
- 正则表达式部分
/*交给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} /*注释和引号中的内容*/
- 每个单词对应的动作
\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上找到对应的回答,比较靠近底部的就是本博文选择的方法,定义了一个函数来更新行号列号。
很多地方并不是很完善。