编译原理实验1:词法分析与语法分析
1 简介
本实验的目标是,对一份输入的源代码,进行词法分析与语法分析,若有错误则输出相关信息,否则输出语法树。
词法分析的思路:编写正则表达式匹配词法单元;将正则表达式转化为不确定的有穷自动机(NFA);将不确定的有穷自动机转化为确定的有穷自动机。
语法分析的思路:从开始状态开始,利用有限状态机理论,根据语言的文法展开式,进行状态分析。
如果从零开始实现完整的词法分析与语法分析,一个学期恐怕是做不完编译器的。所幸,我们可以借助构造词法分析器的工具flex与构造语法分析器的工具bison来实现。(软院选择不借助工具,但是他们一个学期只做了语法分析和词法分析。编译器已经是轮子了,在轮子的轮子上耗费太多精力,感觉不太值得。)
1.1 完成的功能
必做功能
识别词法错误:出现任何未定义字符与任何不符合词法单元定义的字符
识别语法错误
选做功能
识别8进制与16进制数
识别指数形式的浮点数
识别“//”与“/*…*/”形式的注释
1.2 编译步骤
bison -d ex1.y
flex ex1.l
gcc main.c ex1.tab.c tree.c -lfl -ly -o parser
1.3 程序结构
main.c
程序入口,读入文件,调用bison内部函数进行语法分析
tree.h与tree.c
定义了多叉树的节点类型;定义了插入节点与打印语法树的函数。
lexx.yy.c
由flex编译ex1.l得到,进行词法分析
ex1.tab.h与ex1.tab.c
由bison编译ex1.y得到,进行语法分析
2 关键功能与实现方法
2.1 词法分析
2.1.1 综述
词法分析部分依据实验参考书中附录A c–语言文法,A.1.1 Tokens,按flex规则编写正则表达式匹配各种词法单元。
2.1.2 普通tokens
普通的tokens正则表达式较为简单,不再赘述。这里主要讲识别tokens之后的动作。
一个普通的规则如下:
";" {yylval.type_Node_ptr=(struct Node *)malloc(sizeof(struct Node));
yylval.type_Node_ptr->lineNum=yylineno;yylval.type_Node_ptr->type=1;return SEMI;}
匹配一个token之后的动作包括:
* 新建多叉树节点
* 记录当前token的行号
* 指定当前的token类型,tree.h中定义了48个type与数字1~48分别对应。
* 返回当前词法单元
其中,yylval是flex的内部变量,它的类型是我在bison代码中定义的union类型。
%union{
TreeNode* type_Node_ptr;
}
需要注意的是,不能直接将strut Node *赋值给yylval,需要赋值给yylval.type_Node_ptr,否则编译不过。
2.1.3 8进制数/16进制数与科学计数法
匹配10进制/8进制/16进制数的正则表达式:
(0|[1-9]{digit}{0,31})|(0[xX](0|[1-9a-fA-F]{digit_16}*))|(0(0|[1-7][0-7]*))
匹配float与科学计数法的正则表达式:
((0|[1-9]{digit}*)\.{digit}*)|([0-9]*\.[0-9]*[eE][+-]?[0-9]+)
匹配token后,要将它们的数值存储进结点中。
首先将char* 转换为int或float。此时,常用的atoi()与atof()功能就不够了。
查阅资料,找到了下面两个更加灵活的转换函数:
long int strtol(const char *nptr,char **endptr,int base);
float strtof(const char *nptr, char **endptr);
其中,strtol的参数base表示进制,设置base为0,将默认为10进制,并自动识别0x开头的16进制,0开头的8进制。
2.1.4 注释
两种风格的注释本来都是比较难的部分。
不过//风格注释的处理,作为input函数的例子出现在了实验指导书P20,不再赘述。
/* */形式的注释
第一反应是写出简单的非贪婪匹配\/\*(.|\n)*?\*\/但这是无效的。
这样的写法无法识别p11的例1.10的嵌套注释的错误,这与c–的语法要求不一致。
显然,这里代表非贪婪模式的*?没有被识别。
查阅资料发现,flex的正则表达式是不支持非贪婪模式的,这里必须使用比较复杂的方法来解决。
基于一个简单的思路:"/*"(非(*/))*"*/"即可解决这一问题。
难点在于,非(*/) 的表示。
这里用到了一个小技巧:将其分解为两种情况:
1. 任何非*字符
2. *后面连接一个非/字符
于是,非(*/)可以表示为[^*]|(\*[^"/"]
完整的正则表达式可写成:\/\*([^*]|(\*[^"/"]))*\*\/
2.1.5 其它
2.1.5.1 isError与error type A
在定义部分声明变量int isError=0;
在所有tokens的最后,添加如下代码:
. {
isError=1;
printf("Error type A at line %d:Mysterious charasters \'%s\'\n",yylineno,yytext);
}
进行错误类型A的处理,并对isError进行标记。
这样就不会遇到error后还打印语法树的信息了。
2.1.5.2 YY_USER_ACTION
YY_USER_ACTION宏表示在执行每一个动作之前需要被执行的一段代码,默认为空,这里依据实验指导书P31,使其维护位置信息。
需要注意两点:
#define YY_USER_ACTION后面如果换行,需要在后面加上\。实验指导书的排版具有误导性。
bison有一个选项%locations,如果在bison中使用了@n访问位置信息,这个选项会被默认开启。但如果没有@n,就要手动开启这个选项才能使用内置变量yylloc。
2.1.5.3 空格,不可见字符,tab的处理
由于这些字符不应当进入语法分析器,因此不作处理。
但完全忽视的话,它们会被当成A类型error。
因此,需要在规则部分对这些字符进行匹配,但不进行处理。
2.1.5.4 token的顺序
因为flex优先匹配更长/靠前的token,例如ID写到TYPE前面,int/float将被匹配为ID。因此,需要适当调整token的顺序,保证每个token都能被匹配。
2.2 语法分析
2.2.1 综述
词法分析部分依据实验参考书中附录A c–语言文法,A.1.2~A.1.7,按上下文无关文法编写规则匹配各种语法单元。
2.2.2 普通语法单元
一个普通的语法单元结构如下:
ExtDefList:{$$=createNode(0);$$->lineNum=@$.first_line;$$->type=29;}
|ExtDef ExtDefList{$$=createNode(2,$1,$2);$$->lineNum=@$.first_line;$$->type=29;}
;
将构成它的语法单元作为子节点,构造新的结点。并记录行号以及结点类型。
2.2.3 结点类型YYSTYPE
每个终结符或非终结符的类型由宏YYSTYPE定义。
我们使用bison内置的机制对YYSTYPE进行定义:
在定义部分添加如下代码
%union{ TreeNode* type_Node_ptr; }
其实用union主要是为了让不同的语法单元具有不同的属性值类型,在这里我全部定义为TreeNode*,本来没必要使用union。但我试图将YYSTYPE直接定义为TreeNode*时,flex中的内置变量yylval出现错误。
2.2.4 错误处理
在bison的用户函数部分重定义函数yyerror:
yyerror(char* str){
isError=1;
printf("Error type B at line %d:%s\n",yylineno,str);
}
按要求输出Error type B的相关信息。
将isError置为1。
通过查阅bison manul,
4 Parser C-Language Interface
4.3 The Error Reporting Function yyerror
了解到,在定义部分添加%error-verbose,bison将提供更加详细的错误信息。
添加此选项前后,输出的错误信息分别是:
Error type B at line *:syntax error
Error type B at line *:syntax error, unexpected ****
阅读源码发现,在一定条件下,bison将提供更多信息,如missing ****。
2.2.5 打印语法树
在开始符号program对应的语义动作中,判断isError,如果为0,则调用printTree()打印语法树。
Program:ExtDefList {$$=createNode(1,$1);$$->lineNum=@$.first_line;$$->type=28;if(!isError) printTree($$);}
2.3 语法树
2.3.1 定义的数据结构
结点的结构体中定义了以下属性:行号,子结点数目,结点类型,数值,字符串属性(ID,TYPE,RELOP需要用到),以及指向子结点的指针。
二维数组Node_types用于表明结点类型序号与字符的关系,具体定义如下:
char Node_types[50][20]={"","SEMI","COMMA","ASSIGNOP","PLUS","MINUS",
"STAR","DIV","AND","OR","DOT","NOT","LP","RP","LB","RB","LC","RC",
"RELOP","IF","ELSE","WHILE","STRUCT","type","TYPE","INT","FLOAT","ID",
"Program","ExtDefList","ExtDef","ExtDecList","Specifier","StructSpecifier",
"OptTag","Tag","VarDec","Fundec","VarList","ParamDec","CompSt","StmtList",
"Stmt","DefList","Def","DecList","Dec","Exp","Args"};
共48个类型,其中1~27为终结符,28~48为非终结符,这样便于判断。
2.3.2 创建新结点
创建新结点:createNode(int childNum,...)
这是一个可变参数的函数,在#include 后使用。这样在创建新结点的时候更加灵活。
2.3.3 打印语法树-逻辑
定义全局变量tabcnt,初始化为0。用来指示打印空格的数量。
打印语法树的逻辑如下:
* 先判断当前结点,如果是非终结符,且子结点数量为0,直接返回。
否则,打印2倍tabCnt数量的空格。
* 打印当前结点类型
* 判断,如果是非终结符,打印行号
* 判断,如果是RELOP,TYPE,ID,打印字符信息
* 判断,如果是INT或FLOAT,打印数值
* 打印换行
* tabcnt++
* 依次打印子结点信息
* tabcnt--
3 结语
通过本次实验,更加深刻地理解了词法分析与语法分析的过程。
提高了c语言应用能力。
4 参考文献