前言:实验不是很难,主要考察正则表达式部分
lab1实验报告
实验要求
根据cminux-f
的词法补全lexical_analyer.l
文件,完成词法分析器,能够输出识别出的token
,type
,line
(刚出现的行数),pos_start
(该行开始位置),pos_end
(结束的位置,不包含)
文本输入:
int a;
则识别结果应为:
int 280 1 2 5
a 285 1 6 7
; 270 1 7 8
对于部分token,我们只需要进行过滤,即只需被识别,但是不应该被输出到分析结果中。因为这些token对程序运行不起到任何作用。
实验难点
- 实验环境的搭建以及配置,比如ubuntu18.04的安装以及flex编译环境的配置等。
- 学习并掌握cminus-f语法,提供其词法的正则表达式。
- 学会利用FLEX并使用正则表达式来编写正确的词法分析器程序,能够识别各种词语
实验设计
1.实验要求能够识别出所有的输入token,但并不是所有的内容都需要识别。具体需要识别的token在 lexical_analyzer.h 定义了,打开lexical_analyzer.h 并查看具体要识别的token以及其对应的字符和含义,符号的编号和具体内容如下:
//运算
ADD = 259, /* 加号:+ */
SUB = 260, /* 减号:- */
MUL = 261, /* 乘号:* */
DIV = 262, /* 除法:/ */
LT = 263, /* 小于:< */
LTE = 264, /* 小于等于:<= */
GT = 265, /* 大于:> */
GTE = 266, /* 大于等于:>= */
EQ = 267, /* 相等:== */
NEQ = 268, /* 不相等:!= */
ASSIN = 269,/* 单个等于号:= */
//符号
SEMICOLON = 270, /* 分号:; */
COMMA = 271, /* 逗号:, */
LPARENTHESE = 272, /* 左括号:( */
RPARENTHESE = 273, /* 右括号:) */
LBRACKET = 274, /* 左中括号:[ */
RBRACKET = 275, /* 右中括号:] */
LBRACE = 276, /* 左大括号:{ */
RBRACE = 277, /* 右大括号:} */
//关键字
ELSE = 278, /* else */
IF = 279, /* if */
INT = 280, /* int */
FLOAT = 281, /* float */
RETURN = 282, /* return */
VOID = 283, /* void */
WHILE = 284, /* while */
//ID和NUM
IDENTIFIER = 285, /* 变量名,例如a,b */
INTEGER = 286, /* 整数,例如1,2 */
FLOATPOINT = 287, /* 浮点数,例如1.1,1.2 */
ARRAY = 288, /* 数组,例如[] */
LETTER = 289, /* 单个字母,例如a,z */
//others
EOL = 290, /* 换行符,\n或\0 */
COMMENT = 291, /* 注释 */
BLANK = 292, /* 空格 */
ERROR = 258 /* 错误 */
2.知道了要识别的token的具体含义,就可以写出其对应的正则表达式。参考 C-Minus文法 来写出各个要识别的token的正则表达式。
因此,token的type和对应的正则表达式如下,其中:
-
对于运算符,符号等token的正则表达式即为其对应的英文状态下的半角符号。
-
对于关键字等token,在c-minus-f中的语法中的正则表达式为其对应的小写状态。
-
接着ID和NUM的正则表达式比较复杂,使用语法糖可以有效来简化构造。
-
对于变量名,可以是a-z或A到Z之间的一个或多个字母的组合,从a-z和A-Z中选一个的正则表达式为[a-zA-Z],选一个或多个的话加一个+号即可,即[a-zA-Z]+。
-
对于整数,就是从0-9里选一个或多个数,则正则表达式为[0-9]+,也可以使用[\d]+(不知道c-minus-f支不支持)。
-
对于浮点数,有一个问题就是算不算正负数,这里我觉得应该是不算的,如果是负的浮点数前面的符号应该当做‘-’来匹配。要注意的一点就是.号前要加\转义符,否则会将.当做任意字符来匹配。小数点前应该有1或多个数组,不然比如
.123
这样也不叫浮点数,小数点后同理。 -
数组这里也有一个坑,就是[和]和[]是三个东西(题目也给出了),如果要匹配数组[],则两个框都要加转义符从而可以匹配。
-
-
其他符号
-
换行符的正则表达式就是\n,由于可以实现一到多行的换行,所以要在后面加个+(实现一个或多个换行)。
-
注释符的正则表达式是最复杂的,题目要求注释直接不能有嵌套,可能比如类似
/* */ */
这样。首先注释的开头和结尾为/*
和*/
,由于都是特殊字符,所以都需要加上转义符用于匹配。在中间部分,想法就是如果开始注释符/*
之后的注释内容中要么就不包含*,要么就如果包含了一个或多个 * 的话,其后面的注释内容不能跟着*/
(因为最末尾还有着*/作为注释的结束符作为匹配)。所以中间注释内容部分,要么不含 * ,可以用[^\*]
表示,\是转义符,代表着可以匹配除 * 以外的所有内容。如果包含 * ,可以是一个或多个,那么可以用 * 的闭包表示,然后其后接的符号不能为*/
,所以最后的注释的正则表达式为\/\*([^\*]|(\*)*[^\*\/])*(\*)*\*\/
-
空白符的正则表达式可以有很多种,比如正则表达式手则中可以这么表示。实际上也可以直接打一个空格
" "
也能当做空白符的正则表达式
-
对于error错误,我认识error错误可以匹配任何字符,所以用.来表示错误,代表 匹配除“
\n
”之外的任何单个字符。
-
对于写出的正则表达式,可以在 正则表达式在线测试 | 菜鸟工具 (runoob.com) 进行测试。
token | type | 正则表达式 |
---|---|---|
ADD | 259 | + |
SUB | 260 | - |
MUL | 261 | * |
DIV | 262 | / |
LT | 263 | < |
LTE | 264 | <= |
GT | 265 | > |
GTE | 266 | >= |
EQ | 267 | == |
NEQ | 268 | != |
ASSIN | 269 | = |
SEMICOLON | 270 | ; |
COMMA | 271 | , |
LPARENTHESE | 272 | ( |
RPARENTHESE | 273 | ) |
LBRACKET | 274 | [ |
RBRACKET | 275 | ] |
LBRACE | 276 | { |
RBRACE | 277 | } |
ELSE | 278 | else |
IF | 279 | if |
INT | 280 | int |
FLOAT | 281 | float |
RETURN | 282 | return |
VOID | 283 | void |
WHILE | 284 | while |
IDENTIFIER | 285 | [a-zA-Z]+ |
INTEGER | 286 | [0-9]+ |
FLOATPOINT | 287 | [0-9]+.[0-9]+ |
ARRAY | 288 | \[\] |
LETTER | 289 | [a-zA-Z] |
EOL | 290 | [\n]+ |
COMMENT | 291 | `/*([^*] |
BLANK | 292 | [ \f\n\r\t\v] |
ERROR | 258 | . |
3.写出指定模式匹配时对应的动作
实验文档的基础知识中给出了如何写一个简单的单词数量统计的程序
%{
//在%{和%}中的代码会被原样照抄到生成的lex.yy.c文件的开头,您可以在这里书写声明与定义
#include <string.h>
int chars = 0;
int words = 0;
%}
%%
/*你可以在这里使用你熟悉的正则表达式来编写模式*/
/*你可以用C代码来指定模式匹配时对应的动作*/
/*yytext指针指向本次匹配的输入文本*/
/*左部分([a-zA-Z]+)为要匹配的正则表达式,
右部分({ chars += strlen(yytext);words++;})为匹配到该正则表达式后执行的动作*/
[a-zA-Z]+ { chars += strlen(yytext);words++;}
. {}
/*对其他所有字符,不做处理,继续执行*/
%%
int main(int argc, char **argv){
//yylex()是flex提供的词法分析例程,默认读取stdin
yylex();
printf("look, I find %d words of %d chars\n", words, chars);
return 0;
}
可以看到,在第二部分要求我们使用熟悉的正则表达式来编写模式,这也是题目要求补全lexical_analyer.l文件中的一部分内容,那么可以仿照上面的程序来编写出对应的动作
yytext指针指向本次匹配的输入文本,我们在左部分写要匹配的正则表达式,右部分为匹配到该正则表达式后执行的动作。 除了一些特殊的字符要在analyzer
函数中实现外,其余部分的实现都是差不多的。题目要求能够输出识别出的token,type ,line(刚出现的行数),pos_start(该行开始位置),pos_end(结束的位置,不包含) 。首先将开始位置和结束位置设置成一样,代表当前识别的字符从上一识别完的字符的末尾开始。接着设置pos_end+=strlen(yytext)
,strlen(yytext)为这次识别到的长度,然后返回识别出的token即可。
代码如下:
/****请在此补全所有flex的模式与动作 start******/
//STUDENT TO DO
/******** 运算 ********/
\+ {pos_start=pos_end;pos_end+=strlen(yytext);return ADD;}
\- {pos_start=pos_end;pos_end+=strlen(yytext);return SUB;}
\* {pos_start=pos_end;pos_end+=strlen(yytext);return MUL;}
\/ {pos_start=pos_end;pos_end+=strlen(yytext);return DIV;}
\< {pos_start=pos_end;pos_end+=strlen(yytext);return LT;}
\<\= {pos_start=pos_end;pos_end+=strlen(yytext);return LTE;}
\> {pos_start=pos_end;pos_end+=strlen(yytext);return GT;}
\>\= {pos_start=pos_end;pos_end+=strlen(yytext);return GTE;}
\=\= {pos_start=pos_end;pos_end+=strlen(yytext);return EQ;}
\!\= {pos_start=pos_end;pos_end+=strlen(yytext);return NEQ;}
\= {pos_start=pos_end;pos_end+=strlen(yytext);return ASSIN;}
/******** 符号 ********/
\; {pos_start=pos_end;pos_end+=strlen(yytext);return SEMICOLON;}
\, {pos_start=pos_end;pos_end+=strlen(yytext);return COMMA;}
\( {pos_start=pos_end;pos_end+=strlen(yytext);return LPARENTHESE;}
\) {pos_start=pos_end;pos_end+=strlen(yytext);return RPARENTHESE;}
\[ {pos_start=pos_end;pos_end+=strlen(yytext);return LBRACKET;}
\] {pos_start=pos_end;pos_end+=strlen(yytext);return RBRACKET;}
\{ {pos_start=pos_end;pos_end+=strlen(yytext);return LBRACE;}
\} {pos_start=pos_end;pos_end+=strlen(yytext);return RBRACE;}
/******** 关键字 ********/
else {pos_start=pos_end;pos_end+=strlen(yytext);return ELSE;}
if {pos_start=pos_end;pos_end+=strlen(yytext);return IF;}
int {pos_start=pos_end;pos_end+=strlen(yytext);return INT;}
float {pos_start=pos_end;pos_end+=strlen(yytext);return FLOAT;}
return {pos_start=pos_end;pos_end+=strlen(yytext);return RETURN;}
void {pos_start=pos_end;pos_end+=strlen(yytext);return VOID;}
while {pos_start=pos_end;pos_end+=strlen(yytext);return WHILE;}
/******** ID和NUM ********/
[a-zA-Z]+ {pos_start=pos_end;pos_end+=pos_start+strlen(yytext);return IDENTIFIER;}
[0-9]+ {pos_start=pos_end;pos_end+=pos_start+strlen(yytext);return INTEGER;}
[0-9]+\.[0-9]+ {pos_start=pos_end;pos_end+=pos_start+strlen(yytext);return FLOATPOINT;}
\[\] {pos_start=pos_end;pos_end+=strlen(yytext);return ARRAY;}
[a-zA-Z] {pos_start=pos_end;pos_end+=strlen(yytext);return LETTER;}
/******** others ********/
[\n]+ {pos_start = 1;pos_end = 1;lines+=strlen(yytext);return EOL;}
\/\*([^\*]|(\*)*[^\*\/])*(\*)*\*\/ {return COMMENT;}
[ \f\n\r\t\v] {pos_start = pos_end;pos_end+=strlen(yytext);return BLANK;}
. {return ERROR;}
/****请在此补全所有flex的模式与动作 end******/
4.补充 analyzer
函数
实验要求补充完整lexical_analyer.l文件,其中flex的模式与动作只是一部分,另一部分就是在analyzer
函数中补充关于匹配到注释COMMENT,空格BLANK以及换行EOL后执行的动作。由于在上一步已经写了关于空格BLANK以及换行EOL后执行的动作,所以这里只用写注释COMMENT的动作即可,其他留空就行。因为在步骤2时已经return ,所以这里只需要写关于pos_start,pos_end以及lines的相关变化
- 对于注释COMMENT,先获yytext的长度为len,然后通过while循环判断注释中是否存在换行符,即判断yytext[i]是否为换行符,如果是换行\n的话,将pos_start和pos_end设置为1,lines加1;否则pos_end++;当循环结束则break;
case COMMENT:
//STUDENT TO DO
for(int i = 0;i < strlen(yytext);i++)
{
pos_end++;
if( yytext[i] == '\n' )
{
lines += 1;pos_end = 1;
}
}
break;
实验结果验证
1.创建build文件夹,配置编译环境, 执行指令make lexer
运行代码,开始编译,如图所示,编译成功
2.执行指令python3 ./tests/lab1/test_lexer.py
来查看是否通过6个测试样例 。可以看到,通过了6个测试样例
3.验证结果正确性。输入diff ./tests/lab1/token ./tests/lab1/TA_token
将自己的生成结果和助教提供的TA_token
进行比较。如果结果完全正确,则没有任何输出结果。如果有不一致,则会汇报具体哪个文件哪部分不一致。如图所示,是正确的。
4.自行设计testcase
进行测试。
代码如下
+-*/<<=>>====!=;()[]{}
if else float return
int a[10];
int a[];
void while
1. 0.123
/*/*test*/*/
运行结果如下./build/lexer ./tests/lab1/testcase/test.cminus out
:
这个测试样例几乎把所有的token都测试了一遍,主要测试的地方如下:
- 对于第一行中的
<=>>====
部分,按照从左往右识别的顺序应该是<=,>,>=,==,=。通过测试结果可以看到可以正确识别出来 - 第二个要测试的地方是关于数组。
[
,]
, 和[]
是三种不同的token。[]
用于声明数组类型,[]
中间不得有空格。 可以看到也能正确识别出来。 - 第三个测试的地方是注释的嵌套。对于
/*/*test*/*/
这个语句,如果支持嵌套注释的话那么/*test*/
是外层注释的注释内容,识别结果应该不会任何内容。但是测试结果显示最后识别出来了*
和/
,说明不支持嵌套注释。 - 对于其他的部分测试结果也能正确查询出来,一些关键字也不会识别成变量名等。有一个问题就是关于浮点型,对于
1.
或0.
这样的数也算作浮点型,这是有点疑问的地方。
实验反馈
这次实验的收获还是挺大的。首先对于正则表达式的各种写法有了更深的理解。比如对于数字的正则表达式,就有[0-9]+
和\d
等几种不同的写法。对于一些比较复杂但有有规律的正则表达式,使用语法糖可以有效的简化结构,比如英文字符,正则表达式为a|b|c|d……|z,使用语法糖可以[a-z]直接表示,十分简便。我还加深了对cminus-f词法的理解。另外我还学会了如何简单使用FLEX,学会了如何利用FLEX
生成一个生成词法分析器,补充完了一个识别token的程序,从而实现对输入的词法进行识别和分析的功能,并能准确给出token所在行,开始位置和结束位置。
同时在做这次实验也遇到了一些困难,不过不是关于实验本身的。第一个问题就是环境的配置,虽然之前使用的Ubuntu18.04,实验推荐的是Ubuntu20.04,并且实验要求下载很多配件,比如flex,LLVM , Bison等。下载过程老是出错,换了很多源找了很多教程才弄好。第二个比较头疼的问题就是gitee的使用问题,由于之前没使用过gitee,对于gitee上的很多操作都很不熟悉,特别是将项目同步上传,试了很久都没有成功。