- 编写一个程序对C–语言(C语言子集)书写的源代码进行词法分析,并打印分析结果。
- 程序要能够检查源代码中可能包含的词法错误:
- 最低要求1.1:能够识别词法中未定义的字符;
- 其他要求1.2:能识别指数形式的浮点数;
- 其他要求1.3:能识别8进制/16进制数。
- 词法分析程序能定位错误位置。
关键字
TOKEN | CONTENT |
---|---|
STRUCT | struct |
RETURN | return |
IF | if |
ELSE | else |
WHILE | while |
TYPE int | float |
特殊符号
TOKEN | CONTENT |
---|---|
SEMI | ; |
COMMA | , |
ASSIGNOP | = |
PLUS | + |
MINUS | - |
STAR | * |
DIV | / |
AND | && |
DOT | . |
NOT | ! |
LP | ( |
RP | ) |
LB | [ |
RB | ] |
LC | { |
RC | } |
数字表示:十进制、二进制、八进制、十六进制、浮点数、科学计数
TOKEN | DESCRIBE | CONTENT |
---|---|---|
INT | 十进制:表示的是所有无符号的整型常数,除“0”外,首位数字不为0 | 0 |
INT_HEX | 十六进制:十六进制以0x或者0X开头(如0xFF32) | 0[xX][a-fA-F0-9]+ |
INT_OCT | 八进制:八进制整数以数字0开头(如:0237) | 0[1-7][0-7]* |
INT_BIN | 二进制:二进制整数以0b或者0B开头 | 0[bB][01]+ |
FLOAT | 浮点数:表示的是所有无符号的浮点型常数,小数点前后必须有数字出现 | {INT}.[0-9]+ |
SCIENCE | 科学计数法:包含基数、指数符号和指数三个部分,基数的小数点可以出现在数字串的任何位 置,指数符号用E或e表示,指数可带±符号或是不带。01.23E2、43.e-4、.5E03都是符合要求的浮点数 | (([0-9]+.[0-9]*) |
标识符
TOKEN | DESCRIBE | CONTENT |
---|---|---|
ID | 标识符由下划线_、数字0-9、字母a-zA-Z组成,开头不能是数字 | [a-z_A-Z][a-z_A-Z0-9]* |
其他字符
TOKEN | DESCRIBE | CONTENT |
---|---|---|
COMMENT | 注释:/…/ | “/"([](([^/])+([/])))"/” |
SPACE | 空白符 | [ \t\n\r\f\v]+ |
AERROR | 其他未匹配字符(用于错误处理) | . |
Lex是LEXical compiler的缩写,是Unix环境下非常著名的工具,主要功能是生成一个词法分析器(scanner)的C源码,描述规则采用正则表达式(regular expression)。描述词法分析器的文件*.l,经过lex编译后,生成一个lex.yy.c 的文件,然后由C编译器编译生成一个词法分析器。词法分析器,简单来说,其任务就是将输入的各种符号,转化成相应的标识符(token),转化后的标识符 很容易被后续阶段处理。
符号 | 含义 |
---|---|
| | 或 |
[] | 括号中的字符取其一 |
- | a-z表示ascii码中介于a-z包括a.z的字符 |
\ | 转义(flex不能识别除字母外的字符) |
* | 0或多个字符 |
? | 0或1个字符 |
+ | 1或多个字符 |
^ | 除此之外的其余字符 |
. | 除\n外的所有字符,等价于^\n |
实验环境:Ubantu 12,已安装gcc
在Linux命令行中安装flex、bison:
sudo apt-get install flex bison
查看版本号,检查是否安装成功:
flex -V
bison -V
写好词法分析的文件(以.l结尾),进入文件所在目录,使用flex进行编译:
flex 词法分析源文件名
编译过后会生成名为lex.yy.c的文件,使用gcc进行编译生成可执行文件:
gcc lex.yy.c -o 可执行文件名
使用编译后的可执行文件分析测试文件的词法:
./可执行文件名 测试文件名
在控制台中查看输出的分析结果
Flex的.l文件内容可以分为三个部分:定义、规则、用户代码
定义:definition
%%
规则:rules
%%
用户代码:code
定义又可以分为四部分:添加头文件、变量声明、设置flex属性、正则表达式定义
/*第一部分 头文件和变量*/
%{
#include
#include
FILE* f;
int i;
int comment_flag;
int comment_begin;
%}
该处声明的变量可以使用在用户代码定义的函数中,作为全局变量
如果变量声明不在%{%}之间,则需要使用tab来缩进
int a;
int b;
flex的选项影响最终生成的词法分析器的属性和行为。这些选项可以在运行flex命令时在终端输入,也可以在.l文件中使用%option指定。
本次实验中需要使用到当前分析的词所在的行数:
/*flex属性,记录符号所在行号*/
%option yylineno
{表达式名称} 正则表达式
在定义正则表达式时可以直接使用已经定义过的表达式名称,需要加上{}
以本次实验中数字类型和标识符为例:
/*第二部分 定义正则表达式*/
/*十进制*/
INT 0|[1-9][0-9]*
/*十六进制*/
INT_HEX 0[xX][a-fA-F0-9]+
/*八进制*/
INT_OCT 0[1-7][0-7]*
/*二进制*/
INT_BIN 0[bB][01]+
/*浮点数*/
FLOAT {INT}\.[0-9]+
/*科学计数法*/
SCIENCE (([0-9]+\.[0-9]*)|([0-9]*\.[0-9]+)|INT)[Ee][-+]?[0-9]+
/*数字类型汇总*/
NUMBER {INT_HEX}|{INT}|{INT_OCT}|{INT_BIN}|{SCIENCE}|{FLOAT}
/*标识符*/
ID [a-z_A-Z][a-z_A-Z0-9]*
匹配关键字:
/*关键字*/
STRUCT struct
RETURN return
IF if
ELSE else
WHILE while
TYPE int|float
/*部分符号*/
/*标点*/
SEMI ;
COMMA ,
/*运算*/
ASSIGNOP =
PLUS \+
MINUS \-
STAR \*
DIV \/
AND &&
DOT \.
NOT \!
/*括号*/
LP \(
RP \)
LB \[
RB \]
LC \{
RC \}
空白符和注释部分正则表达式如下:
/*其它字符*/
/*注释*/
COMMENT ("//".*)|("/*"([*]*(([^*/])+([/])*)*)*"*/")
COMMENT_BEGIN "/*"
/*空白符*/
SPACE [ \f\n\r\t\v]+
/*未定义字符*/
AERROR .
/*十六进制错误*/
INT_HEX_ERROR 0[xX][a-fA-F0-9]*[g-zG-Z]+[a-fA-F0-9]*
/*八进制错误*/
INT_OCT_ERROR 0[0-7]*[89]+[0-7]*
/*二进制错误*/
INT_BIN_ERROR 0[bB][01]*[2-9]+[01]*
当所有的正则表达式都无法匹配当前词时,就会被匹配到AERROR(需要在规则部分的最后进行处理,优先匹配其他类型),从而做出匹配到为定义字符的错误处理
整个规则部分需要包括在
%%
...
%%
并且规则部分中的注释行必须顶一个空格,例如:
/*...*/
编写规则时,一个规则一行,每行有两部分构成:
正则表达式 {动作函数}
也可以使用
正则表达式 |
正则表达式 {...}
此处 “正则表达式” 和 ’|’中间用一个空格隔开
动作函数可以调用在用户代码中定义的函数
以本实验为例:
/*第三部分 操作 action 这里面的注释必须顶格一个空格*/
%%
/*跳过空白和注释*/
{SPACE} {}
{COMMENT} { printf("注释,行数: %d 字符:%s\n",yylineno,yytext);}
{COMMENT_BEGIN} {
comment_flag = 1;
comment_begin = yylineno;}
/*未终结注释错误*/
<> {
if(comment_flag == 1){
printf("UNTERMINATED_COMMENT at line %d\n",yylineno);
comment_flag = 0;}
yyterminate();
}
/*关键字*/
{TYPE} |
{STRUCT} |
{RETURN} |
{IF} |
{ELSE} |
{WHILE} { printf("关键字,行数: %d 字符:%s\n",yylineno,yytext);}
/*数字类型错误*/
{INT_HEX_ERROR} {if(comment_flag!=1) printf("INT_HEX_ERROR at line %d: charachters \"%s\"\n",yylineno,yytext);}
{INT_OCT_ERROR} {if(comment_flag!=1) printf("INT_OCT_ERROR at line %d: charachters \"%s\"\n",yylineno,yytext);}
{INT_BIN_ERROR} {if(comment_flag!=1) printf("INT_BIN_ERROR at line %d: charachters \"%s\"\n",yylineno,yytext);}
/*数字类型表示*/
{NUMBER} {if(comment_flag!=1) printf("数字,行数: %d 字符:%s\n",yylineno,yytext);}
/*标点*/
{SEMI} |
{COMMA} {if(comment_flag!=1) printf("标点,行数: %d 字符:%s\n",yylineno,yytext);}
/*运算符*/
{ASSIGNOP} |
{PLUS} |
{MINUS} |
{STAR} |
{DIV} |
{AND} |
{DOT} |
{NOT} {if(comment_flag!=1) printf("运算符,行数: %d 字符:%s\n",yylineno,yytext);}
/*括号*/
{LP} |
{RP} |
{LB} |
{RB} |
{LC} |
{RC} {if(comment_flag!=1) printf("括号,行数: %d 字符:%s\n",yylineno,yytext);}
/*标识符*/
{ID} {if(comment_flag!=1) printf("标识符,行数:%d 字符:%s\n",yylineno,yytext);}
/*错误*/
{AERROR} {if(comment_flag!=1) printf("Error type A at line %d: mysterious charachter '%s'\n",yylineno,yytext);}
%%
为了展现词法分析效果,将所有匹配情况都进行了输出,实际上只输出AERROR类型即可
这里使用yylineno表示当前所分析的字符串在文件的第几行,yytext是lex内部已经定义好的指针变量,lex分析过程是将输入字符串按程序员预先设计好的正则表达式进行匹配,yytext总是指向当前获得匹配的字符串
该部分必须定义main函数,用于指定需要扫描分析的文件,可以内部指定,也可以通过控制台传入参数:
int main(int argc,char** argv){
if(argc<2){
/*由lex创建的扫描程序的入口点yylex()
调用yylex()启动或者重新开始扫描。
如果lex动作执行讲数值传递给调用的程序return,那么对yylex()的下次调用就从它的停止地方继续。*/
yylex();
return 0;
}
for(i=1;i
这段代码中实现了传入多个文件参数时的处理
yylex():由lex创建的扫描程序的入口点yylex(),调用*yylex()启动或者重新开始扫描。如果lex动作执行讲数值传递给调用的程序return,那么对yylex()*的下次调用就从它的停止地方继续
C 库函数 *void perror(const char str) :把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格
yyrestart():使词法分析器从f中读取标准输入文件
/*第四部分 函数 function*/
int yywrap()
{
/*此函数必须由用户提供,或者声明 %option noyywrap
当词法分析程序遇到文件结尾时,它调用例程yywrap()来找出下一步要做什么
如果返回0,扫描程序继续扫描,如果返回1,扫描程序就返回报告文件结尾*/
return 1;
}
除了定义main函数,用户还需要自己定义yywrap()函数,或者声明 %option noyywrap;当词法分析程序遇到文件结尾时,它调用例程yywrap()来找出下一步要做什么,如果返回0,扫描程序继续扫描,如果返回1,扫描程序就返回报告文件结尾。
完成的内容
- 能够识别C-Minus中定义的词法(按照C-Tokens文件实现)
- 能识指数形式的整数和浮点数(科学计数法)
- 能识别2进制/8进制/16进制数
- 能识别注释内容、空白字符
- 能识别未定义的字符,并定位错误位置
这里留下几个测试文件,读者可自行测试,本人已经测试过了,就不再贴结果
文件1
struct Complex
{
float real, image;
}
int main()
{
struct Complex x;
float a[10] = 1.5;
int i = 100;
x.image = ~i;
if (a[1][2] == 0) i =1 else i =0;
}
预测结果:
第11行存在未定义字符~,其他字符、字符串都可以正常识别
文件2
int main()
{
int i = 0123;
int j = 0x3F;
int k = 0967;
}
预测结果
第五行八进制书写错误,词法分析程序会将0967分析为十进制0和十进制967,交给语法分析器进行进一步处理
本文为原创,如有错误欢迎指正,感谢阅读。
附上参考文献:
编译原理-用FLEX构造词法分析程序、LEX/FLEX词法分析器