本文讲解关于如何用lex工具来编写一个简易版的C语言的词法分析器。我主要通过一个完整的项目例子来进行讲解。当然这篇文章面向已经对lex有所了解但还不会具体运用的读者,如果对lex一无所知,请看我的另一篇文章。
Lex使用讲解
下面进入本文主题。
%{
int wordCounter = 0;
int charCounter=0;
int column =0,line=1;
void addLine(int);
void addColumn(int);
void clearColumn();
void addChar(int);
void addWord(int);
%}
line_comment (\/\/.*\n)
char '[^']'
string \"[^\"]*\"
letter [A-Za-z_]
digit [0-9]
identifier {letter}({letter}|{digit})*
reserveword "break"|"main"|"continue"|"else"|"float"|"for"|"if"|"int"|"return"|"void"|"while"
int {digit}+
float {digit}*(\.{digit}+)?(e|E[+\-]?{digit}+)?
operator "-"|"+"|"="|"=="|"*"|"/"|"<="|">="
delimiters "("|")"|"{"|"}"|";"
il_char \'[^']{4}
il_string \"[^"]{30}
il_identifier ({digit}|{digit}*(\.{digit}+)?(e|E[+\-]?{digit}+)?)({letter}|{digit})*{letter}({letter}|{digit})*
其中在符号%{和%}包裹中的,属于定义段的第一部分,是以C语法写的一些定义和声明,这些定义的变量或是函数是用于记录你将要检验的测试文件中代码的行数已经代码量的,在文章最后项目的结果截图中便可得知其作用。
定义段第二部分,也是关键部分,是正规定义和状态定义,即编写你将要用lex词法分析器识别的信息。其中每一行的开始要顶格写,左侧是将要识别的一种类型的命名(可随意起名),右侧是正则表达式所表示出的规则。
2.2 词法规则段编写
词法规则段列出的是词法分析器需要匹配的正规式,以及匹配该正规式后需要进行的相关动作,代码如下:
%%
{line_comment} {
printf("Line=%3d,Column=%3d : 这是一个注释 %s",line,column,yytext);
addChar(yyleng);
clearColumn();
addLine(1);
}
{char} {
printf("Line=%3d,Column=%3d : 这是一个char型 %s\n",line,column,yytext);
addChar(yyleng);
addColumn(yyleng);
}
{string} {
printf("Line=%3d,Column=%3d : 这是一个string类型 %s\n",line,column,yytext);
addColumn(yyleng);
addChar(yyleng);
}
{reserveword} {
printf("Line=%3d,Column=%3d : 这是关键字 %s\n",line,column,yytext);
addChar(yyleng);
addColumn(yyleng);
addWord(1);
}
{identifier} {
printf("Line=%3d,Column=%3d : 这是一个标识符 %s\n",line,column,yytext);
addChar(yyleng);
addColumn(yyleng);
addWord(1);
}
{int} {
addChar(yyleng);
addColumn(yyleng);
printf("Line=%3d,Column=%3d : 这是一个int型 %s\n",line,column,yytext);
}
{float} {
addChar(yyleng);
addColumn(yyleng);
printf("Line=%3d,Column=%3d : 这是一个float型 %s\n",line,column,yytext);
}
{operator} {
addColumn(1);
addChar(1);
printf("Line=%3d,Column=%3d : 这是一个操作符 %s\n",line,column,yytext);
}
{delimiters} {
addColumn(1);
addChar(1);
printf("Line=%3d,Column=%3d : 这是一个界符 %s\n",line,column,yytext);
}
{il_string} {
addChar(yyleng);
addColumn(yyleng);
printf("错误(Line=%3d,Column=%3d): 错误的string型: %s\n",line,column,yytext);
}
{il_identifier} {
addChar(yyleng);
addColumn(yyleng);
printf("错误(Line=%3d,Column=%3d): 错误的标识符: %s\n",line,column,yytext);
}
{il_char} {
addChar(yyleng);
addColumn(yyleng);
printf("错误(Line=%3d,Column=%3d): 错误的char型: %s\n",line,column,yytext);
}
. {
addChar(1);
addColumn(1);
}
\n {
addChar(1);
addLine(1);
clearColumn();
}
2.3辅助函数段的编写
第三部分是辅助函数段,是用C语言语法来写,辅助函数一般是在词法规则段中用到的函数。这一部分一般会被直接拷贝到lex.yy.c中。代码如下:
%%
int main(void)
{
puts("******************************************");
puts(" 词法分析器 ");
puts("******************************************");
yylex();
puts("******************************************");
puts("计数");
printf("字符数:%5d\n",charCounter);
printf("行数:%5d\n",line);
printf("字数:%5d\n",wordCounter);
puts("******************************************");
return 0;
}
int yywrap() {
return 1;
}
void addLine(int cnt) {
line += cnt;
}
void addColumn(int cnt) {
column += cnt;
}
void clearColumn() {
column = 0;
}
void addChar(int leng) {
charCounter += leng;
}
void addWord(int cnt) {
wordCounter += cnt;
}
在此处应该也可以看出在第一段中编写的那些addLine()等函数的作用了。
到此处为止,一个完整的lex.l的源代码就算编写完成了,剩下要做的只是一些简单的编译操作。
如果你的电脑是Windows系统,需要安装lex环境或在虚拟机中运行,本文当前环境为macOS10.14
首先打开终端,运用cd命令进入lex.l所在位置,然后输入
lex lex.l 或flex lex.l 均可
会自动编译生成lex.yy.c文件,然后输入
gcc lex.yy.c
会自动编译生成a.out文件。a.out即为成功的词法分析器。
接下来先编写好测试文件,放入当前目录下,假设测试文件如下:
#include<cstdio>
#define N 10000
void main(){
int i=2;
int j=4;
int result=0;
while(i <=j){
result=result+j;
i++;
}
printf();
return result;
//注意,在 C- -中,这不是一个打印操作
}
然后输入
./a.out <test.cpp >a.txt
可以重定向,用于将测试文件与词法分析器连接,并将结果输出至a.txt文件中,编写生成的结果如下:
******************************************
词法分析器
******************************************
Line= 1,Column= 1 : 这是一个标识符 include
Line= 1,Column= 9 : 这是一个标识符 cstdio
Line= 2,Column= 1 : 这是一个标识符 define
Line= 2,Column= 8 : 这是一个标识符 N
Line= 2,Column= 15 : 这是一个int型 10000
Line= 4,Column= 0 : 这是关键字 void
Line= 4,Column= 5 : 这是关键字 main
Line= 4,Column= 10 : 这是一个界符 (
Line= 4,Column= 11 : 这是一个界符 )
Line= 4,Column= 12 : 这是一个界符 {
Line= 6,Column= 0 : 这是关键字 int
Line= 6,Column= 4 : 这是一个标识符 i
Line= 6,Column= 6 : 这是一个操作符 =
Line= 6,Column= 7 : 这是一个int型 2
Line= 6,Column= 8 : 这是一个界符 ;
Line= 8,Column= 0 : 这是关键字 int
Line= 8,Column= 4 : 这是一个标识符 j
Line= 8,Column= 6 : 这是一个操作符 =
Line= 8,Column= 7 : 这是一个int型 4
Line= 8,Column= 8 : 这是一个界符 ;
Line= 10,Column= 0 : 这是关键字 int
Line= 10,Column= 4 : 这是一个标识符 result
Line= 10,Column= 11 : 这是一个操作符 =
Line= 10,Column= 12 : 这是一个int型 0
Line= 10,Column= 13 : 这是一个界符 ;
Line= 12,Column= 0 : 这是关键字 while
Line= 12,Column= 6 : 这是一个界符 (
Line= 12,Column= 6 : 这是一个标识符 i
Line= 12,Column= 9 : 这是一个操作符 <=
Line= 12,Column= 9 : 这是一个标识符 j
Line= 12,Column= 11 : 这是一个界符 )
Line= 12,Column= 12 : 这是一个界符 {
Line= 14,Column= 0 : 这是一个标识符 result
Line= 14,Column= 7 : 这是一个操作符 =
Line= 14,Column= 7 : 这是一个标识符 result
Line= 14,Column= 14 : 这是一个操作符 +
Line= 14,Column= 14 : 这是一个标识符 j
Line= 14,Column= 16 : 这是一个界符 ;
Line= 16,Column= 0 : 这是一个标识符 i
Line= 16,Column= 2 : 这是一个操作符 +
Line= 16,Column= 3 : 这是一个操作符 +
Line= 16,Column= 4 : 这是一个界符 ;
Line= 18,Column= 1 : 这是一个界符 }
Line= 20,Column= 0 : 这是一个标识符 printf
Line= 20,Column= 7 : 这是一个界符 (
Line= 20,Column= 8 : 这是一个界符 )
Line= 20,Column= 9 : 这是一个界符 ;
Line= 22,Column= 0 : 这是关键字 return
Line= 22,Column= 7 : 这是一个标识符 result
Line= 22,Column= 14 : 这是一个界符 ;
Line= 24,Column= 0 : 这是一个注释 //注意,在 C- -中,这不是一个打印操作
Line= 26,Column= 1 : 这是一个界符 }
******************************************
计数
字符数: 207
行数: 27
字数: 22
******************************************
到此处为止,一个完整的简易版词法分析器识别的例子就完成了。如果想要包括此项目bnf范式以及文档说明在内的完整项目的,可到一下链接寻找。
完整项目