基于DFA的词法分析(一):程序框架及数据结构
背景:
题目描述:
一、 实验目的
设计、编制并调试一个词法分析程序,加深对词法分析原理的理解。
二、 实验要求
2.1 待分析的简单的词法
(1)关键字:
- begin if then while do end
- 所有的关键字都是小写。
(2)运算符和界符
- : = + - * / < <= <> > >= = ; ( ) #
(3)其他单词是标识符(ID)和整型常数(SUM),通过以下正规式定义:
- ID = letter (letter | digit)*
- NUM = digit digit*
(4)空格有空白、制表符和换行符组成。空格一般用来分隔ID、SUM、运算符、界符和关键字,词法分析阶段通常被忽略。
2.2 各种单词符号对应的种别码:
表2.1 各种单词符号对应的种别码
单词符号 | 种别码 | 单词符号 | 种别码 |
---|---|---|---|
begin | 1 | : | 17 |
if | 2 | := | 18 |
then | 3 | < | 20 |
while | 4 | <> | 21 |
do | 5 | <= | 22 |
end | 6 | > | 23 |
lettet(letter / digit)* | 10 | >= | 24 |
dight dight* | 11 | = | 25 |
+ | 13 | ; | 26 |
— | 14 | ( | 27 |
* | 15 | ) | 28 |
/ | 16 | # | 0 |
2.3 词法分析程序的功能:
输入:所给文法的源程序字符串。
输出:二元组(syn,token或sum)构成的序列。
其中:
- syn为单词种别码;
- token为存放的单词自身字符串;
- sum为整型常数;
- 例如:对源程序begin x:=9: if x>9 then x:=2*x+1/3; end #的源文件,经过词法分析后输出如下序列:
(1,begin)(10,x)(18,:=)(11,9)(26, ; )(2,if)……
1、程序框架设计:
程序的执行过程:
执行程序后,系统开始执行初始化操作,初始化包括:读文件录入种别码表、初始化一些全局变量、读取正则表达式录入有穷字母表(默认已经删去程序中用到的操作符,防止出错),然后就是调用分别再三个头文件按中的函数做正则表达式转NFA、NFA确定化、DFA最小化的工作,然后就得到最小化的DFA,随后利用这个DFA读取待分析的源码做词法分析并输出结果。
按照程序的执行顺序,本程序由以下部分构成:
- 正规表达式的读取与处理,种别码表的制作。
- 正则表达式转NFA
- NFA转DFA
- DFA最小化
- 利用最小化DFA做词法分析
首先是初始化:
利用STL的map结构,first是string类型的字符串second是int类型的种别码,遍历种别码文件将种别码和对应的id分别insert到map中即可。
再遍历一遍正则表达式,利用set结构将正则表达式中所有的字符均插入到set中,然后再删去程序中需要用到的操作符。
然后是构建最小DFA
这部分主要是调用几个头文件中的函数(由组内其他同学完成)
- NFA n = str_To_Nfa(str);//正则表达式转NFA
- DFA d = Nfa_To_Dfa(n);//NFA转DFA
- DFA minDfa = min_DFA(d);//DFA最小化
最后是利用DFA做词法分析
这里的功能封装进了一个函数:
void Judge_file2(const char* fp, DFA minD, map<string, int> List);
函数参数:
- Fp:待分析源码存储的文件名
- MinD:上述得到的最小化DFA
- List:种别码对照表
利用DFA词法分析的方法:
每次从文件中读取一个字符串(以给定的界符为标志分开),依次分析当前字符串的每个字母,先做特殊处理:判断当前字符串是否为操作符(*,|,&),然后开始利用状态转移矩阵做判断,设置一个信号量存储当前状态(初始值为初态),不断循环遍历字符串,依次将当前状态变换成当前状态接受字符串中一个字母后转移到的状态,如果当前状态变为-1表示无法接收,如果一直到单词的末尾都能接受并且当前状态变为DFA的终态之一,则表示当前字符串可以被DFA识别。
对于可以接受的字符串就查表(种别码表),找到与之对应的种别码,然后输出即可。
2、数据结构定义:
本程序用到的数据结构包含以下部分:
- NFA_state_node://标识NFA状态的节点
- NFA://表示一个NFA
- Edge://表示DFA的一条边
- DFA_state_node://标识DFA状态的节点
- DFA://表示一个DFA
数据结构具体设计方法(代码实现):
set<char> All_Char;//所有的操作数集合(从正则表达式读到的所有种类除去操作符)
set<char> Beg;//初态的下一状态集合
set<char> End;//终态的上一状态集合
All_Char是本程序可以识别的所有(除了操作符)操作数的集合,从正则表达式中读取得到。
Beg是初态能接受的串的集合
End是能被接受转移到终态的集合
struct NFA_State_Node /*定义NFA状态*/
{
int state_id ; /*NFA状态的状态号*/
char get_value; /*NFA状态弧上的值*/
int Trans_to; /*NFA状态弧转移到的状态号*/
set<int> E_closure_to; /*当前状态通过ε转移到的状态号集合*/
};
一个NFA状态节点由以下部分构成:状态号、该状态可以接受的字符、该状态能转移到的状态号、还有一个当前状态通过ε转移到的状态号集合。
struct NFA
{
int N_StateNum = 0; /*NFA状态总数*/
set<char> Letter_List; /*NFA的有穷字母表*/
NFA_State_Node* head; /*NFA的头指针*/
NFA_State_Node* tail; /*NFA的尾指针*/
};
NFA_State_Node NFA_States[MAX]; /*NFA状态数组*/
一个NFA由状态数、有穷字母表、头尾指针构成,另外还有一个NFA的状态数组是全局变量(这里本来应该是NFA的成员,但是由于内部含有较多指针,构造函数没有完全设计好,所以这里将其设计为全局的)
class DFA /*定义DFA结构*/
{public:
struct Edge /*定义DFA的转换弧*/
{
char get_value; /*弧上的值*/
int Trans_to; /*弧所指向的状态号*/
};
一条边由该边接受的字符(弧上的值)和该弧转移到的状态构成
struct DFA_State_Node /*定义DFA状态*/
{
int state_id; /*DFA状态的状态号*/
bool isEnd; /*是否为终态,是为true,不是为false*/
set<int> E_closure_to; /*NFA的ε-move()闭包*/
int Edge_Num; /*DFA状态上的射出弧数*/
Edge Edges[MAX]; /*DFA状态上的射出弧*/
};
一个DFA状态节点由以下部分构成:状态号、是否为终态的标志、该状态上的弧的个数、该状态上的弧数组、还有一个当前状态通过ε转移到的状态号集合。
int Beg_State; /*DFA的初态*/
set<int> End_States; /*DFA的终态集*/
int D_StateNum = 0; /*DFA状态总数*/
set<char> Letter_List; /*DFA的有穷字母表*/
int trans[MAX][MAX]; /*DFA的转移矩阵*/
DFA_State_Node DFA_States[MAX]; /*DFA状态数组*/
一个DFA由状态数、有穷字母表、初态编号、终态集合、一个DFA的状态数组和转移矩阵构成
状态转移矩阵的含义:
状态1通过接收字母b到达状态二,表示为:
Ans.trans[1][(int)b-(int)*All_Char.begin()] == 2;
重点是trans的第二维的意义,由于其是int类型所以只能用ascii码来表示,
故用(int)将char类型强制类型转换成对应的ascii码,
但是由于有穷字母表一般不能包含所有ascii码表,所以会造成一定的空间的浪费(最小的ascii码前面的所有空间都被浪费)
所以我们实际上采用的是某个字符对应有穷字母表内ascii码最小字符的相对位置来判断的
所以字符b在转移矩阵中的位置应该是(int)b-(int)*All_Char.begin()
DFA() {
memset(trans, -1, sizeof(trans)); /*初始化dfa的转移矩阵*/
for (int i = 0; i < MAX; i++)
{
DFA_States[i].state_id = i;
DFA_States[i].isEnd = false;
for (int j = 0; j < MAX; j++)
{
DFA_States[i].Edges[j].get_value = '#';
DFA_States[i].Edges[j].Trans_to = -1;
}
}
}
};
在DFA构造函数中初始化各个成员
其他算法的实现:
基于DFA的词法分析(二):构造DFA