词法分析器概述
词法分析器的本质:基本任务是进行模式匹配,其关键在于分析过程中的模式说明和模式识别方法,在编译分析中即正规表达式和有限自动机。
构造词法分析器方法: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;
6、mininum 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构造转换表Dtran,DFA的每个状态是NFA的状态集,D将并行地模拟N对输入串的所有可能的移动。 A、构造NFA N的状态K的子集的算法 假定所构造的子集族为C(D的状态集合),即C= (T1, T2,,... TI),其中T1, T2,,... TI为状态S的子集。 开始,令e-closure(S0)为C中唯一成员,并且它是未被标记的。
B、e-closure的计算
|
·DFA的最小化:利用可区分的概念,将所有不可区分的状态看作是一个状态
输入:DFA M(其状态集合为S),输入符号为∑,转换函数为f:S×∑--〉S,开始状态为s0 ,接受状态集为F。 输出:一个DFA M’,它和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++ 进行编程。正规式与自动机理论在词法构造乃至整个编译器构造过程中起着至关重要的作用,同时它们被广泛应用于计算机科学的各个领域,它们与计算机其它学科之间也有着很大的联系。