PA2中使用flex
生成一个词法分析器(lexical analyzer,又称scanner)。
我花了大概4-5天,30个小时左右,困扰我的主要是以下两点:
- 一开始不知道要做什么
- 正则表达式的匹配问题
得分:
flex
.flex
文件的格式如下:
%{
Declarations
%}
Definitions
%%
Rules
%%
User subroutines
其中:
Declarations
中可以用C++
写声明,可以在这里面添加一些变量用于后面。Definitions
中可以给一段正则表达式命名,这样使程序可读性更强,如DIGIT [0-9]
,格式与宏定义类似,详见这里。Rules
中写正则表达式以及匹配时执行的代码,详见这里和几个例子。User subroutines
可以不用管。
主要文件
cool.flex
需要补充的文件,在这里面填写正则表达式和代码等内容。
test.cl
用于测试的程序,但是不能涵盖所有情况,可以使用判题脚本进行更加全面地检查。
文件缺失问题
打开PA2
下的README
,它会让你检查你的文件夹下面是否有指定的几个文件,我使用的是老师提供的环境,缺少了几个文件,这些文件在这个包中可以找到:
链接:https://pan.baidu.com/s/13Sbfxah3E5PWwGpbwy2tVA
提取码:2paa
将缺少的文件放到共享文件夹中,然后在虚拟机中移动到正确的文件夹下就可以了。
准备
输出Hello World
通过一个简单的输出来熟悉一下做作业流程。
打开cool.flex
,在rule
处添加如下内容,该句中.*
的意思是匹配任意一行,若匹配成功,则打印Hello World!
:
.* { cout << "Hello World!"; }
在命令行处输入make lexer
,生成词法分析器:
输入make dotest
,进行测试:
样例中的每一行都会匹配.*
,因此每行都导致输出Hello World!
。
执行perl pa1-grading.pl
判分,这行命令会用你写的规则生成词法分析器,并用更多的测试用例测试:
不通过的用例会像上面图片中一样显示,可以在grading
目录下查看脚本中的测试用例,以及你的输出(第一次运行脚本后,grading
目录就会出现)。
除此之外,为了详细地了解如何做以及做的对不对,我们可以用老师已经实现的一个词法分析器来标准输出,然后用标准输出与我们的输出进行比对——
老师实现的词法分析器:
对test.cl
运行这个分析器,输入../../bin/reflexer test.cl
:
通过比对自己的输出以及标准输出,来修改cool.flex
。
作业流程
流程:修改cool.flex
文件、判分、修改、重复直到满分。
下面从简单到困难,完成各个部分正则表达式的编写。
关键字和一些符号
所有关键字、符号及其代表的值可以在cool-parse.h
文件中找到。
除了true
和false
的其他关键字大小写不敏感,而true
和false
的首字母必须小写。
阅读flex
文档的Patterns部分,想要匹配大小写不敏感的字母,需要在前面添加?i:
,如匹配大小写不敏感的class
,要写:
?:class
因此,在Definition
部分中写如下内容(包括多字符操作符),注意true
、false
和其他关键字格式不同:
DARROW =>
ASSIGN <-
LE <=
CLASS (?i:class)
INHERITS (?i:inherits)
IF (?i:if)
THEN (?i:then)
ELSE (?i:else)
FI (?i:fi)
WHILE (?i:while)
LOOP (?i:loop)
POOL (?i:pool)
LET (?i:let)
IN (?i:in)
FALSE (f(?i:alse))
TRUE (t(?i:rue))
ISVOID (?i:isvoid)
CASE (?i:case)
ESAC (?i:esac)
NEW (?i:new)
OF (?i:of)
NOT (?i:not)
然后在rules
部分添加:
{DARROW} { return DARROW; }
{ASSIGN} { return ASSIGN; }
{LE} { return LE; }
{CLASS} { return CLASS; }
{INHERITS} { return INHERITS; }
{IF} { return IF; }
{THEN} { return THEN; }
{ELSE} { return ELSE; }
{FI} { return FI; }
{WHILE} { return WHILE; }
{LOOP} { return LOOP; }
{POOL} { return POOL; }
{LET} { return LET; }
{IN} { return IN; }
{ISVOID} { return ISVOID; }
{CASE} { return CASE; }
{ESAC} { return ESAC; }
{NEW} { return NEW; }
{OF} { return OF; }
{NOT} { return NOT; }
{TRUE} {
cool_yylval.boolean = true;
return BOOL_CONST;
}
{FALSE} {
cool_yylval.boolean = false;
return BOOL_CONST;
}
要注意的是左侧{}
中的单词与右侧返回的单词不是一个东西,左侧的代表的是我们刚刚在Definition
中定义的正则表达式,而右侧的是C++
语言中的宏定义,其实质是整数。
除此之外,理解一下这段是如何运行的,.cool
文件作为输入,程序会调用int yylex()
函数(关于这个函数,看这里),用正则表达式匹配输入的字符,若某一部分匹配成功,则执行右侧的代码,而yylex()
的返回值就是代码中的返回值,如return NOT;
就让yylex()
返回了一个整数,这个整数代表关键字not
。
yylex
会反复执行直到读取文件结束。
整数Integer
当遇到整数时,代码中先在inttable
中记录这个数字,然后返回INT_CONST
,其指明这是一个整型:
[0-9]+ {
cool_yylval.symbol = inttable.add_string(yytext);
return INT_CONST;
}
注意:
- 关于
inttable
,请看Tour of Cool Support Code。 - 关于
cool_yylval
,请看老师发的PA1作业说明以及flex
文档。
标识符identier
包括类型名和变量名两类:
类型名
大写字母开头,跟数字、字母、下划线:
[A-Z][a-zA-Z0-9_]* {
cool_yylval.symbol = idtable.add_string(yytext);
return TYPEID;
}
变量名
小写字母开头,跟数字、字母、下划线:
[a-z][a-zA-Z0-9_]* {
cool_yylval.symbol = idtable.add_string(yytext);
return OBJECTID;
}
操作符
多字符操作符,如<-
,已经在关键字部分中写好了,剩下还有.
、@
、~
、+
、-
、*
、\
、=
、<
:
[\.@~] { return yytext[0]; }
[\+\-\*\/\=\<] { return yytext[0]; }
其中yytext
的类型是char*
,即字符串,它的内容就是匹配成功的字符串。
标点符号
这部分也比较简单:
[\{\}\:\;\(\)\,] { return yytext[0]; }
空白符号
包括5
个符号,遇到\n
行数增加,遇到其他空白符跳过就可以了,被跳过的字符会原封不动地被打印在输出中。
\n { line_num++; curr_lineno = line_num; }
[ \f\r\t\v] /* --skip-- */
注意:
line_num
是自己定义的一个全局变量,在Declaration
部分添加就可以了。curr_lineno
是flex
给我们提供的一个全局变量,用于记录当前的行数。
无效字符
这部分必须放在整个文件的最后,匹配所有没被正确的表达式匹配的字符。
. {
yylval.error_msg = yytext;
return ERROR;
}
注释
单行注释
--.* /* ---skip--- */
.
不会匹配换行符\n
,因此这样写只会匹配一行。
多行注释
多行注释是比较难的部分。
在开始前,需要阅读flex
文档中的第10章,了解一下这个flex
的语法糖,这大大方便了我们的编写。
Cool
语言的注释匹配模式与C/C++
不同,看一个对比的例子:
(* (* *)
/* /* */
第一行的Cool
注释报错,而第三行的C
注释不会报错,这是因为Cool
语言的注释会检查匹配的层数,(*
有2个,但*)
仅一个,不匹配。
为此,我们需要一个变量记录注释的层数,具体的规则如下所示:
- 每遇到一个
(*
层数加1
,遇到一个*)
层数减1
- 层数大于
0
表示在注释模式中 - 层数从大于
0
减至0
则退出注释模式 - 若在一般模式下遇到
*)
则报错 - 到文件结束时若层数依然大于
0
则报错
我们需要表示层数以及注释模式,这可以通过一个变量以及上面所说的语法糖实现,在Declaration
中添加int deepth = 0
表示层数,在Definition
中添加:
%x COMMENT
用COMMENT
表示注释模式。
至此,正则表达式如下:
"(*" { deepth = 1; BEGIN(COMMENT); }
<COMMENT><<EOF>> {
cool_yylval.error_msg = "EOF in comment";
BEGIN(INITIAL);
return ERROR;
}
<COMMENT>"(*" { deepth++; }
<COMMENT>"*)" {
--deepth;
if(deepth == 0)
BEGIN(INITIAL);
}
<COMMENT>\\.
<COMMENT>\n { line_num++; }
<COMMENT>"*"[^\)\*\n] /* ---- eat anything ---- */
<COMMENT>. /* ---- eat anything ---- */
\*\) {
cool_yylval.error_msg = "Unmatched *)";
return (ERROR);
}
字符串
字符串匹配是最难的部分,一些规则老师没有在文档中说明。
这部分地匹配与上面的注释匹配比较类似,难点是对于'\0'
的匹配、以及各种报错的先后顺序问题。
当在INITIAL
的模式下遇到了"
,那么就进入字符串匹配模式,在Definition
中添加:
%x STR
由于字符串中可能会遇到转义字符,我们再定义一个转义字符模式:
%x STR_ESCAPE
我们还需要一个char*
数组记录字符串,i
表示当前的位置下标:
char* my_string;
int i;
在STR
模式下进行处理的规则:
- 遇到
"
,说明字符串结束了,代码中判断字符串是否合法,若非法则报错,否则将字符串添加到stringtable
中。 - 遇到
'\n'
,立即报错。 - 遇到
'\0'
,做一个记录,但不报错,继续读取字符串。 - 遇到
'\'
,进入STR_ESCAPE
模式。 - 遇到
EOF
即文件结束了,立即报错。 - 遇到其他字符,则在
my_string
中添加。 - 若字符串长度超过限度,则做一个记录,不报错,继续读取。
在STR_ESCAPE
模式下:
- 遇到
n
、b
、f
、t
中的一个(记为x),则在my_string
中添加'\x'
,返回STR
模式 - 遇到
'\0'
,做一个记录,返回STR
模式 - 遇到
EOF
,报错 - 遇到其他字符(记为x),在
my_string
中添加'x'
,返回到STR
模式。
报错规则和顺序如下:
- 在读取过程中遇到了
\n
,则立即报Unterminated string constant
,并返回INITIAL
模式 - 读取过程中若检查到字符串中有
\0
,则在读取结束时报String contains null character
- 读取过程中若检查到字符串过长,则在读取结束时报
String constant too long
最后,该部分代码实现如下:
{%
char* my_string;
int i;
#define MAX_ERROR_NUMBER 4
int* error_queue; /*错误队列*/
int error_size; /*记录错误个数*/
bool containNull;
%}
\" {
my_string = new char[MAX_STR_CONST];
error_queue = new int[MAX_ERROR_NUMBER];
i = 0;
error_size = 0;
BEGIN(STR);
}
<STR>\" {
curr_lineno = line_num;
if (error_size > 0) {
int error = error_queue[0];
if (error == 1 || error == 3) {
containNull = false;
if(error == 1) yylval.error_msg = "String contains null character";
else yylval.error_msg = "String contains escaped null character";
BEGIN(INITIAL);
return ERROR;
}
if (error == 2) {
yylval.error_msg = "String constant too long";
BEGIN(INITIAL);
return ERROR;
}
}
yylval.symbol = stringtable.add_string(my_string);
BEGIN(INITIAL);
return STR_CONST;
}
<STR>\\ { BEGIN(STR_ESCAPE); }
<STR,STR_ESCAPE><<EOF>> {
yylval.error_msg = "EOF in string constant";
BEGIN(INITIAL);
return ERROR;
}
<STR_ESCAPE>[nbft] {
if (i < MAX_STR_CONST - 1) {
char cur = yytext[0];
if (cur == 'n') my_string[i++] = '\n';
else if (cur == 'b') my_string[i++] = '\b';
else if (cur == 'f') my_string[i++] = '\f';
else my_string[i++] = '\t';
}
else {
if(error_size < MAX_ERROR_NUMBER)
error_queue[error_size] = 2;
error_size++;
}
BEGIN(STR);
}
<STR_ESCAPE>\0 {
containNull = true;
if(error_size < MAX_ERROR_NUMBER)
error_queue[error_size] = 3;
error_size++;
BEGIN(STR);
}
<STR_ESCAPE>[^\0] {
if (i < MAX_STR_CONST - 1) {
if(yytext[0] == '\n') line_num++;
my_string[i++] = yytext[0];
BEGIN(STR);
}
else {
if(error_size < MAX_ERROR_NUMBER)
error_queue[error_size] = 2;
error_size++;
}
}
<STR>\0 {
containNull = true;
if(error_size < MAX_ERROR_NUMBER)
error_queue[error_size] = 1;
error_size++;
}
<STR>\n {
line_num++;
curr_lineno = line_num;
if (containNull) {
containNull = false;
yylval.error_msg = "String contains null character";
BEGIN(INITIAL);
return ERROR;
}
yylval.error_msg = "Unterminated string constant";
BEGIN(INITIAL);
return ERROR;
}
<STR>. {
if (i < MAX_STR_CONST - 1) {
my_string[i++] = yytext[0];
}
else {
if(error_size < MAX_ERROR_NUMBER)
error_queue[error_size] = 2;
error_size++;
}
}