Learning Traces...

--Great Love involves great effort
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

用C++编写简单绘图语言的词法分析器

Posted on 2008-03-26 14:57  suyang  阅读(8747)  评论(3编辑  收藏  举报

  词法分析器概述

词法分析器的本质:基本任务是进行模式匹配,其关键在于分析过程中的模式说明和模式识别方法,在编译分析中即正规表达式和有限自动机。

构造词法分析器方法:1、手工构造;2、利用自动生成工具LEX。但是无论用那种方法,其内在工作原理都是相同的,都要经过正规式到最小状态DFA的转换。

词法分析器可有两种:一种是把词法分析器作为语法分析的一个子程序,一种是把词法分析器作为编译程序的独立一遍.在前一种情况下,词法分析器不断地被语法分析器调用,每调用一次词法分析器将从源程序的字符序列拼出一个单词,并将其Token值返回给语法分析器.后一种情况则不同,词法分析器不是被语法分析器不断地调用,而是一次扫描全部单词完成编译器的独立一遍任务。

一、任务与目的

·任务:

1、使用C/C++程序设计语言和递归下降子程序的方法编写该函数绘图语言的词法分析器。并要求设计一个词法分析器的测试小程序来调用自己编写的词法分析器测试各种不同的输入。

2、词法分析的任务是对输入的字符串形式的源程序按顺序进行扫描,在扫描的同时,根据源语言的词法规则识别具有独立意义的单词(符号),并产生与其等价的属性字流(内部编码)作为输出。通常属性字流即是对识别的单词给出的标记符号的集合。

·目的:

通过自己动手编写词法分析器,掌握记号、模式与单词,掌握正规式与正规集,掌握有限自动机,掌握如何从正规式到词法分析器的各种算法。理解如何理论联系实际以及明白理论与实际的差别。

二、分析与设计

    词法分析程序一般具有如下功能:读入字符串形式的源程序;识别出具有独立意义的最小语法单位:单词。

    事实上,由正规表达式到最小化DFA的转换源程序中的测试生成串部分就是对所输入的单词进行判断,看其是否能被生成的DFA接受(也就是这个单词是否符合正规式定义的要求)。这本质上就是一个简单的词法分析。

定义某种语言的单词,并给出编号。该语言单词包括:保留字、运算符、标识符、常量、格式符等。根据给定的语言子集构造词法分析器。输出为中间文件。

在设计时为了便于理解,不使用内部编码而用枚举对同类型的单词进行标识。例如所有的常量统一用“CONST_ID”对其进行标识,当扫描时遇到常量就输出该常量的值和“CONST_ID”标识。

这里给出词法分析程序大概的设计方法:

             1、根据要求写出词法分析的正规文法G

             2、根据正规文法G,写出正则式RE

             3、根据正则式RE,画出NFA

             4、将NFA转化为DFA

             5、将DFA转化为mininum state DFA

             6mininum state DFA就是词法分析程序的流程图,根据此流程图编写相应的词     法分析程序。

以下是较为详细的设计:

①总体结构与模块划分

测试模块(scannermain.cpp

词法分析器模块(scanner.h & scanner.cpp

②重要数据结构

·枚举记号种类

enum Token_Type{

     ORIGIN, SCALE, ROT, IS, TO, STEP, DRAW, FOR, FROM,                    // 保留字

     T,                                                                   // 参数

     SEMICO, L_BRACKET, R_BRACKET, COMMA,                                  // 分隔符号

     PLUS, MINUS, MUL, DIV, POWER,                                        // 运算符

     FUNC,                                                                // 函数

     CONST_ID,                                                            // 常数

     NONTOKEN,                                                            // 空记号

     ERRTOKEN                                                             // 出错记号

};

·记号与符号表结构

struct Token{

     Token_Type type;                                             // 记号的类别

     char *lexeme;                                                // 构成记号的字符串

     double value;                                                // 若为常数,则是常数的值

     MathFuncPtr FuncPtr;                                         // 若为函数,则是函数的指针

};

·符号表

static Token TokenTab[] = {

     {CONST_ID,    "PI",         3.1415926,    0   },

     {CONST_ID,    "E",          2.71828,     0   },

     {T,           "T",          0.0,          0   },

     {FUNC,        "SIN",        0.0,          sin },

     {FUNC,        "COS",        0.0,          cos },

     {FUNC,        "TAN",        0.0,          tan },

     {FUNC,        "LN",         0.0,          log },

     {FUNC,        "EXP",        0.0,          exp },

     {FUNC,        "SQRT",       0.0,          sqrt},

     {ORIGIN,     "ORIGIN",     0.0,          0   },

     {SCALE,       "SCALE",     0.0,          0   },

     {ROT,         "ROT",        0.0,          0   },

     {IS,          "IS",         0.0,          0   },

     {FOR,         "FOR",        0.0,          0   },

     {FROM,        "FROM",       0.0,          0   },

     {TO,          "TO",         0.0,          0   },

     {STEP,        "STEP",       0.0,          0   },

     {DRAW,        "DRAW",       0.0,          0   }

};

③关键思想与算法

·构造NFA的Thompson算法

·模拟NFA的“并行”算法

·从NFA构造DFA:构造DFA的子集法,smove(S, a)函数和e_闭包(T)的计算

输入:一个NFA N

输出:一个接受同样语言的DFA D

方法:D构造转换表DtranDFA的每个状态是NFA的状态集,D将并行地模拟N对输入串的所有可能的移动。

A、构造NFA N状态K的子集的算法

    假定所构造的子集族为CD的状态集合),即C= (T1, T2,,... TI),其中T1, T2,,... TI为状态S的子集。

    开始,令e-closure(S0)C中唯一成员,并且它是未被标记的。

whileC中存在尚未被标记的子集Tdo

{     

    标记T

    for 每个输入字母a do {

        U:= e-closure(move(T,a))

        if U不在C then

            U作为未标记的子集加在C中;

        Dtran[Ta]:=U       

}

}

Be-closure的计算

T中所有的状态压入栈stack中;

e-closureT)初始化为T

While stack不空do

begin

    将栈顶元素t弹出栈;

    for每个这样的状态u:从tu有一条标记为e的边do

       if u不在e-closureT)内 do

        begin

            u添加到e-closureT);

            u压入栈stack

        End

End

·DFA的最小化:利用可区分的概念,将所有不可区分的状态看作是一个状态

输入:DFA M(其状态集合为S),输入符号为∑,转换函数为f:S×∑--〉S,开始状态为s0 ,接受状态集为F。

输出:一个DFA M’,它和M接受同样的语言,且状态数最少。

算法

1、构造具有两个组的状态集合的初始划分∏:接受状态组F,非接受状态组S-F。

2、对∏采用下面所述的过程来构造新的划分∏new。

for ∏中的每个组G do

begin

当且仅当对任意输入符号a,状态s和t在a上的转换到达∏中的同一组中的状态时,才把G划分成小组,以便G的两个状态s和t在同一小组中;

/*最坏情况下,一个状态就可能成为一个组*/

用所有新形成的小组集代替∏new中的G;

end

3、如果∏new=∏,令∏final=∏,再执行步骤4;否则,令∏:=∏new,重复步骤2。

4、在划分∏final的每个状态组中选一个状态作为该组的代表,这些代表构成了简化后的DFA M’的状态。令s是一个代表状态,而且假设:在DFA M中,在输入a上有从s到t的转换。令t所在组的代表是r(r可能就是t),那么在M’中有一个从s到r的a上的转换。令包含s0 的状态组的代表是M’的开始状态,并令M’的接受状态是那些属于F集的状态所在组的代表。注意,∏final的每个组或者仅含F中的状态,或者不含F中的状态。

5、如果M’含有死状态(即一个对所有输入符号都有到自身的转换的非接受状态d),即从M’中去掉它;删除从开始状态不可到达的状态;取消从任何其它状态到死状态的转换定义。

·两种类型的词法分析器:表驱动型与直接编码型号;这里使用直接编码型

三、测试例程设计

·测试程序(scannermain.cpp)

int main(int argc, char *argv[]) {

     Token token;

     if(!InitScanner("test.txt")) {

         printf("Open Source File Error !"n");

         return -1;

     }

     printf("记号类别     字符串    常数值        函数指针"n");

     printf("________________________________________________"n");

     while(1) {

         token = GetToken();

         if(token.type != NONTOKEN)

              printf("%4d, %12s, %12f, %12x"n", token.type, token.lexeme, token.value, token.FuncPtr);

         else break;

     };

     printf("________________________________________________"n");

     CloseScanner();

     return 0;

}

·测试数据(test.txt)

FOR to ORIGIN draw IS step       --keyword                  

0.12 9.9912 123 PI e             //constant

SIN tan COS ln EXP               --function

; , ** ( ) - + / *               //symbol

TO98 id id1 suyang               --illegal

718shen 100rot     -1            //special

四、测试结果及分析

·结果分析

    该词法分析器的输出为一堆记号流,这些记号流正确的反映出了绘图语言源程序中的各个单词的类型。例如:“FOR”被识别为“关键字”类别;“SUYANG”被识别为错误的TOKEN等等。并且对注释性语句也正确的识别了。

    在测试过程中需要说明三点出现的问题及错误:

1、“**”POWER的正确识别

……

if(Char == '*') {

     token.type = POWER;

     AddCharTokenString(Char);//《〈编译原理基础〉习题与上机题解答》中没有该行,加上后才正确

     break;

}

……

错误不严重,只是"**"会错误的显示为"*"

2、和开发环境有关的错误

……

for(int loop = 0; loop < sizeof(TokenTab) / sizeof(sizeof(TokenTab[0])) / 6; ++loop) {

                       /* 注意:需要"... / 6",否则 TokenTab 中的元素个数扩大了 6 倍 */

     if(strcmp(TokenTab[loop].lexeme, IDString) == 0) return TokenTab[loop];

}

……

    这个问题只针对我的机器,在别的机器上不一定会出现该问题,我想是由于硬件平台的问题(可能和我的CPU是双核有关)。

3、在VS2005中生成工程时会产生两个警告(C4996, C4313

1>------ 已启动全部重新生成: 项目: 词法分析器, 配置: Debug Win32 ------

1>正在编译...

1>scanner.cpp

1>d:"函数绘图语言编译器构造"词法分析器"scanner.cpp(15) : warning C4996: fopen被声明为否决的

1>d:"program files"microsoft visual studio 8"vc"include"stdio.h(234) : 参见fopen的声明

1>消息:This function or variable may be unsafe. Consider using fopen_s instead. To disable deprecation, use _CRT_SECURE_NO_DEPRECATE. See online help for details.

1>scannermain.cpp

1>d:"函数绘图语言编译器构造"词法分析器"scannermain.cpp(19) : warning C4313: printf: 格式字符串中的%x与参数4 (属于MathFuncPtr类型)冲突

1>正在生成代码...

1>正在编译资源清单...

1>正在链接...

1>LINK : 没有找到D:"函数绘图语言编译器构造"Debug"词法分析器.exe 或上一个增量链接没有生成它;正在执行完全链接

1>正在嵌入清单...

1>生成日志保存在file://d:"函数绘图语言编译器构造"词法分析器"Debug"BuildLog.htm

1>词法分析器- 0 个错误,2个警告

    这个问题比较容易解决,只需简单的屏蔽掉这两个警告即可。在scanner.h文件中加上以下两条语句:

#ifndef SCANNER_H

#pragma warning (disable:4996)                                   // 屏蔽警告

#pragma warning (disable:4313)                                   // 屏蔽警告

……

五、总结与体会

    主要学习和体会了基于编译器构造技术中的由正规表达式到最小化DFA的算法设计和实现技术;主要包括由正规表达式构造NFA所用到的Thompson构造法、把NFA转化为与其等价的DFA所使用的子集构造算法以及把DFA最小化的算法,最后实现词法分析。Thompson构造法根据读入的正规表达式的不同字符进入相应的转换处理。NFA转化为与其等价的DFA需分两步进行:a、构造NFA N的状态K的子集的算法b、计算e-closure。完成这些子模块的设计后,再通过某一中间模块的总控程序对其调用,最后再由主程序合并调用。在算法实现过程中,主要使用visual C++进行编程。正规式与自动机理论在词法构造乃至整个编译器构造过程中起着至关重要的作用,同时它们被广泛应用于计算机科学的各个领域,它们与计算机其它学科之间也有着很大的联系。

附:源代码清单