代码改变世界

Flex词法分析器使用

2009-04-06 08:36  Iron  阅读(5857)  评论(0编辑  收藏  举报

感谢:http://blog.csdn.net/litchh/archive/2004/07/14/40983.aspx

在构造编译器方面,lex和yacc可谓是有很大的天赋,lex主要用于词法的分析,而yacc则用于语法语义等分析。

那么究竟如何使用lex和yacc呢?如果你是一个老鸟,当然这篇文章你可以跳过不看,如果你是一个新手,那么看了下面这个例子我想你会对flex做词法分析了解很多。

下面我通过一个例子来详细说明如何使用flex

根据所学的词法分析内容,利用flex构造PL/0语言的词法分析器。

既然是构造PL/0的词法分析器,那么我们有必要看一下pl0语言的简介和相应文法:

2       

PL/0语言

.PL/0语言概述.

    PL/0语言是PASCAL语言的子集,它具备一般高级程序设计语言的典型特点。PL/0语言编译程序结构比较清晰,可读性强,充分体现了一个高级语言编译程序实现的基本组织、技术和步骤,是一个非常合适的小型编译程序的教学模型。

 

.PL/0语言文法描述.

   由于只是做PL/0的词法分析,因此这里只列出词法有关的内容,其余略过。

   下面用扩充的EBNF来进行表示:

   元符号说明:

< >’:用左右尖括号括起来的中文字表示语法构造成分,或称语法单位,是PL/0的非终结符

   ::=’:该符号的左部由右部定义。

   |’:表示“或”,即作不可由多个右部定义。

   { }’:表示花括号内的语法成分可以重复。在不加上下界时可以重复0到任意次数,有上下界时为可重复次数的限制。

   [ ]’:表示方括号内的成分为任选项。

   ‘()’:表示圆括号内的成分优先。

  

   PL/0语言文法的EBNF表示

   <常量定义>::=<标识符>=<无符号整数>

   <无符号数字>::=<数字>{<数字>}

   <标识符>::=<字母>{<字母>|<数字>}

   <加法运算符>::=+|-

   <乘法运算符>::=*|/

   <关系运算符>::==|#|<|<=|>|>=

   <分界符>::=(|)|,|;|.

 

2       

flex词法生成器

.flex概要.

flex是一个用于生成扫描器的工具,扫描器可识别文本中的词法模式。flex从给定的文件中读取,或者从标准输入中读取(当没有给定文件时)有关要生成的扫描器的说明。这种说明的格式是一对正规表达式和C代码,称之为规则。flex的输出是名为lexyy.cC源程序,在lexyy.c中定义了一个名为yylex()的函数。lexyy.c可以被编译,并使用-lfl链接选项同flex库链接,以生成可执行文件。执行该文件,它会分析它的输入,察看是否满足正规表达式,只要它发现一个,就会执行相应的C代码。

 

. Flex运行与应用过程.

     Flexlex的功能、结构几乎完全相同。Lex编译器接收lex源程序(该源程序是对要产生的词法分析器的说明和描述),由lex编译器处理lex源程序后产生一个词法分析器作为输出。一般说来,lex源程序经过Lex编译器处理后产生一个lexyy.c的程序,这就是对应的词法分析器程序,经过C编译器编译后就可以产生一个可执行程序。通过这个可执行程序我们就可以对相应的程序语言做词法分析

 

.flex语言及结构.

Flex源语言结构

说明部分      /* 包含模式宏定义和C语言的说明信息*/

%%

规则部分      /*  转换规则(一般还包含规则对应的动作)*/

%%

用户代码      /*  规则动作部分所需的辅助过程的C代码 */

Flex语言是对表示语言单词集的正规式的描述,以解决正规式规则输入问题。由上图可知Flex语言作为词法分析器自动构造的专用语言,其程序结构由三部分组成。其中,第一部分说明部分包含C代码、模式宏定义等。模式宏定义实际是对识别规则中出现的正规式的辅助的影。如语言的字母可定义为:

Letter [a-zA-Z]

数字可以定义为:

digit[0-9]

   除宏定义外,定义部分的其余代码必须用符号%{ %} 括起来。另外,flex使用的C语言库文件和外部变量以及部分声明的函数,也应分别置于%{ %} 之内。例如下面是一个flex语言的说明部分:

%{ 

include<stdio.h>

include<stdlib.h>

int flag;

void function( );

#define err -1

%}

digit[0-9]

letter[a-zA-Z]

newline [\n]

%%

注意这里其标识符作用的%%%{ %}必须要顶行写,另外,在其中也可以随意添加C语言的注释。

    第二部分,识别的规则部分是flex程序的主体部分。其一般形式是

             模式1   动作1

             模式2   动作2

                   ……

             模式n   模式n

其中模式是对要分析语言的单词的描述,用正规表达式表示。动作是与匹配的模式对应的,一般用C代码(这要看相应的支持平台)表示对模式处理的动作。当识别出某个模式的单词后,词法分析器(flex)要做的动作就是执行相应的程序。详细的模式这里不再给出,flex手册会有比较详细的定义。

    第三部分,即用户代码定义部分,它是对模式进行处理的C函数、主函数等。作为辅助过程它是支持规则动作部分所需要的处理过程,是对规则部分中动作的补充。这些过程如果不是C的库函数,必须给出具体的定义,然后分别编译且与生成的词法分析器装配在一起。

    需要说明的一点是,定义部分和用户代码部分是任选的,规则部分则是必须的。

 

 .lexyy.c的全局变量和函数.

     需要说明的是在flex生成的lexyy.c中,里面是有一些是本身就定义好的一些变量和函数,这些变量和函数对我们完成词法分析的操作有很大的帮助,这里主要介绍这次实验要用到的和常用的一些函数和变量。

     File*yyin   /* 指向词法分析器要接收的待分析程序的指针。如果不指定则默认指向标准输入终端(键盘)。如果我们待分析的程序是文件形式我们可以将这个指针指向该文件的地址指针。 */

File*yyout  /* 同上,唯一不同是该指针指向输出的文件。默认指向标准输出终端(屏幕)。我们可通过重定向该指针改变输出流方向。 */

char*yytext  /* 指向识别的单词的地址;用来保存扫描一次匹配的字符串。*/

int yyleng    /* 匹配的字符串中字符的个数。*/

 

函数

ECHO  /* flex的默认动作,一般来说是输出字符串 */

yywrap() /* 扫描一次完后要调用的函数,返回一个值,当这个值为1的时候分flex就不再继续扫描。*/

yyrestart() /* 重新定向flex的输入 */

当然还有其它很多的函数,比如yymore()、相容分组、条件模式、宏等,这里由于本次实验没有用到,因此不再详细给出。具体可以查看flex手册。

 

.flex词法分析产生器实现概述。

   词法分析器自动生成器的核心是lex编译器,lex编译器的功能是对某语言单词集描述的lex源程序,将其变换为一个能识别该语言单词的词法分析器。而该词法分析器像有限自动机一样取识别处理单词。

   基于lex源程序,lex编译器的实现步骤大致是:

   ⑴对lex源程序识别规则中的每个pi构造一个相应的NFA  Ni

     ⑵引入唯一初态S,从初态S通过ε弧将所有NFA  Nii=1n)连接成新的NFA N’。⑴、⑵两步实际是完成从正规表达式到非确定有限自动机的构造。

   ⑶对NFA  N’确定化,产生DFA  N

   DFA  N 最小化。

   ⑸给出控制程序。控制程序的作用是激活有限自动机,即控制输入字符串在有限自动机上运行,一旦达到终态,即识别出lex源程序模式描述的某个单词,转去调用相应的动作部分就可以了。

 

到这里我想应该对flex都有了一定了解了吧,现在我们就开始着手编写flex分析pl/0的输入文件lex.l文件,实际上这个文件的作用就是如何识别pl/0的词法,也就是识别pl/0词法规则的说明文件。从上面讲的我们知道这样的一个文件分为3部分,下面我来详细说明每一部分:

 

㈠说明部分.

      由于词法分析最后的结果是一个二元组,并且本次实验只是给出一个形式化的显示,但是为了更加的形象,因此我用了一个struct结构来描述这个二元组,并且当分析完后将token存放到这个二元组中。另外还有一个函数print()的声明,该函数的主要作用是输出识别到的token。另外还定义一些辅助操作的变量。例如用于读写文件的指针,保存行号、token数目、错误操作符数目的变量。

      接下来是PL/0词法的描述,用正规表达式书写:

      digit         [0-9]     /*数字从0-9*/

letter        [a-zA-Z]  /*字母接受a-z的大小写,同时本程序定义PL/0语言大小写敏感;*/

number        {digit}+  /*无符号整数*/

identifier    {letter}({letter}|{digit})*  /*标识符*/

wrongid       ({digit}+){letter}({letter}|{digit})*   /*错误的id,形如123a*/

newline       [\n]  /*新行*/

whitespace    [\t]+ /*制表符*/

    

    ㈡规则部分.

      由于说明部分我们已经定义好无符号整数和标识符的正规表达时,并且用了一个别名,而保留字和运算符、分界符比较少,因此规则部分我们就直接根据每个模式写出要执行的对应的动作。在这里我定义的动作就是先将每种单词分种类,然后同一种给一个整数最为代号,然后在print()函数中根据这个代号输出相应的属性。因此动作部分的主要处理工作就是赋值,调用print()函数。

例如: {identifier}       {value=1;print();}

      详细的PL/0词法的保留字、分界符、运算符见后面的附录。

  

 ㈢用户代码.

   用户代码部分主要包括三个函数:

   主函数main()  //给出源文件输入输出的方向,调用扫描器等;

   输出处理函数print()//主要功能就是输出二元组;

   yywrap()   //自带函数,用来结束扫描;

要注意空格和制表符,因为在源程序中为了排版等不可避免的存在空格和制表符,但是在实际的词法分析中它们是没有具体意义的,因此读到空格或指标符的时候要“吃掉”他们。

注意newlinenewline就是换行符,但我们读到换行符的时候,应当对其执行空格和制表符的操作(“吃掉”),但是我们要统计行数,应此我们设立一个变量每当读到一个换行符的时候就递增1

另外,lex在读到匹配模式的单词后就执行模式对应的动作,但是它会默认地将不匹配的单词输出,因此我们要对分析语言没有的符号进行另外的处理(例如打印出错等)。


void main(int argc,char*argv[]);         //主函数;
struct token{                            //二元组;
      char*idproperty;    //token属性值;
      char*idname;    //识别的token名字;
}entity[1000];     //定义1000个这样的token,大小可改变;
char*filename;                           //保存结果的文件名;
int errnum=0;     //错误token的数目;
int value;     //属性值int型;
int linenum=1;     //行数;
int count=0;     //token的个数;
int flag=0;
FILE*fpin;     //测试文件指针;
FILE*fpout;     //结果文件指针;
%}
digit         [0-9]
letter        [a-zA-Z]
number        {digit}+
identifier    {letter}({letter}|{digit})*
wrongid       ({digit}+){letter}({letter}|{digit})*
newline       [\n]
whitespace    [\t]+
%%
"procedure" | 
"call"  |        
"begin"  |  
"end"  |  
"var"  |  
"const"  |  
"if"  |  
"then"  |  
"while"  |  
"do"  |  
"read"  |  
"write"  |  
"odd"    {value=0;print();}
{identifier}   {value=1;print();}
{wrongid}   {value=6;print();}
{number}   {value=2;print();}
"+"|"-"|"*"|"/"          {value=3;print();}
"<>"  |  
">="  |  
"<="  |  
":="  |  
"="|"#"|"<"|">"          {value=4;print();}
"("|")"|","|";" |
"."                             {value=5;print();}
{newline}   {linenum+=1;}
{whitespace}   {;}
" "    {;}
.    {value=7;print();}
%%
int yywrap()
{  
    fclose(fpin);
    return 1;
}


{
    count+=1;
    if(flag!=1){
       if((fpout=fopen(filename,"a"))==NULL){
           printf("cannot write the file \n");
           exit(0);
       }
    }
    if(value<=5){
       switch(value){
             case 0:entity[count-1].idproperty="BasicKey";break;
             case 1:entity[count-1].idproperty="identifier";break;
             case 2:entity[count-1].idproperty="number";break;
             case 3:entity[count-1].idproperty="arithmetic-op";break;
             case 4:entity[count-1].idproperty="relation-op";break;
             case 5:entity[count-1].idproperty="boundary-op";break;
       }
       entity[count-1].idname=yytext;
       fprintf(fpout,"%d < %s , %s > \n",count,entity[count-1].idname,entity[count-1].idproperty);
    }else{
         errnum+=1;
         switch(value){
               case 6:entity[count-1].idproperty="Mixed number and letter:";break;
               case 7:entity[count-1].idproperty="Unkown operator:";break;
         }
         entity[count-1].idname=yytext;
         fprintf(fpout,"%d [line:%d]:%s\"%s\" \n",count,linenum,entity[count-1].idproperty,entity[count-1].idname);
    }
    if(flag!=1)fclose(fpout);
}


{
    if(argc==1){
      printf("please input the PL\\0 program(ctrl+z to end) \n");
      flag=1;
      fpin=stdin;
      fpout=stdout;
    }
    if(argc==2)argv[2]="defresult.txt";
    filename=argv[2];
    if(flag!=1){
       if((fpin=fopen(argv[1],"r"))==NULL){
           printf("cannot open the file \n");
           exit(0);
       }
    }
    yyin=fpin;
    yylex();
    if(flag!=1){
       if((fpout=fopen(filename,"a"))==NULL){
           printf("cannot write the file \n");
           exit(0);
       }
    }
    fprintf(fpout,"\n");
    fprintf(fpout,"%d symbol(s) found. \n %d error(s) found. \n",count,errnum);
    fprintf(fpout,"======================================================================= \n");
    if(flag!=1)fclose(fpout);
    yywrap();


}