实验目的
- 掌握生成词法分析器的方法,加深对词法分析原理的理解。
- 掌握设计、编制并调试词法分析程序的思想和方法。
- 本实验是高级语言程序设计、数据结构和编译原理中词法分析原理等知识的综合。
实验内容
布置内容及要求
-
选择一种熟悉的高级语言(如C语言,C++,VB或VC等),设计、编写一个词法分析子程序。
-
待分析的源程序为一个简单的C语言程序,如下所示:
void test(int a, int b, float c, float d) { int x; float y; x = a + b; y = c / d; if(x>y) x = 10; else y = 12.5; }将该源程序的源文件经词法分析后输出以二元组形式表示的单词符号序列。
-
编写的程序具有一定的查错能力。提交的实验报告中要有实验名称、实验目的、实验内容、实验程序清单、调试过程和运行结果,程序的主要部分作出功能说明,并有实验收获体会或改进意见等内容。
-
实验程序完成后在实验课上让老师检查通过后,将实验报告和程序打包发送给课代表;邮件命名规则:学号.姓名.实验一,例如:0925111016.陈培发.实验一,每个实验占编译原理平时成绩的15分(总分30),另外编译实验课本身也独立给分。
个人理解
- 使用Java语言(个人选择),实现正规式处理、Thompson算法、子集构造算法、DFA最小化算法、表驱动算法这五大块,根据老师评分标准结合正规式处理和Thompson算法,以四个类体现
- 表驱动算法需加入一定检测错误能力,个人在其中加入了错误字符识别抛弃功能
实验程序清单
自定义数据结构
//定义正规式类型
class RegularExpression {
String content;
String type;
}
//定义后缀正规式类型
class SuffixExpression {
LinkedList<Character> suffixExpression;
String type;
}
//定义Thompson片段
class Thompson_Node {
int start;
int end;
}
//定义NFA构造元素类,便于多NFA组合使用,额外加入Thompson_Node便于指向起止点位置
class ElementNFA {
NFA_Node[] vertexes;
String[][] edges;
MatchTypeNFA[] matchTypes;
char[] enterChars;
Thompson_Node tn;
}
//NFA节点类型
class NFA_Node {
//节点名称(转移状态名)
int position;
//接受值
char receivedValue;
}
//NFA类型匹配对象
class MatchTypeNFA {
//节点名称
int position;
//对象名
String type;
}
//NFA图类型
public class NFA {
//邻接表
LinkedList<NFA_Node>[] vertexLists;
//邻接表长度,也即状态(节点)数
int size;
//匹配类型,子集构造算法将会传递此参数
MatchTypeNFA[] matchTypes;
// NFA_Node[] vertexes;
//输入字符集合
char[] enterChars;
//节点状态hash表
HashMap<Integer, Character> map;
}
//DFA节点类型
class DFA_Node {
//节点名称(转移状态名)
char position;
//接受值
char receivedValue;
}
//DFA类型匹配对象
class MatchTypeDFA {
//节点名称
char position;
//对象名
String type;
}
//DFA图类型
public class DFA {
//邻接表
LinkedList<DFA_Node>[] vertexLists;
//邻接表长度,也即状态(节点)数
int size;
//匹配类型,表驱动算法将会用到此参数
MatchTypeDFA[] matchTypes;
//节点列表,最小化算法将引用此参数
DFA_Node[] vertexes;
}
//定义TokenStream类用于存储二元组
class TokenStream {
String type;
String word;
}
主函数
//正规式
RegularExpression[] res = new RegularExpression[]{new RegularExpression("(0-9)(0-9)*", "DIGIT"), new RegularExpression("(0-9)(0-9)*.(0-9)(0-9)*", "DECIMAL"), new RegularExpression("(a-z)(a-z)*(_(a-z)(a-z)*)*", "ID")};
//将正规式转为后缀正规式
SuffixExpression[] ses = new SuffixExpression[res.length];
for (int i = 0; i < res.length; i++) {
ses[i] = new SuffixExpression(res[i]);
}
//构造Thompson类对象
Thompson thompson = new Thompson();
//组合所需NFA元素
ElementNFA elementNFA = thompson.constructNFA(ses[0]);
for (int i = 1; i < ses.length; i++) {
elementNFA = thompson.combineNFA(elementNFA, thompson.constructNFA(ses[i]));
}
//生成NFA
NFA nfa = new NFA(elementNFA.vertexes, elementNFA.edges, elementNFA.matchTypes, elementNFA.enterChars);
nfa.print(nfa.vertexLists);
//子集构造生成DFA
DFA dfa = nfa.subsetConstruct();
//调用最小化DFA方法
dfa.minimizeDFA();
//定义表驱动类对象,读取文件并输出二元组
TableDriven tableDriven = new TableDriven(dfa);
tableDriven.readC("./Cproject.c");
调用函数清单
//构造正规式
public RegularExpression(String content, String type) {}
/**
* 将中缀正规式转为后缀正规式
*
* @param re 待处理中缀正规式
* @return 后缀正规式链表
*/
public SuffixExpression(RegularExpression re) {}
/**
* 根据正规式构造NFA
*
* @param se 后缀正规式
* @return 返回NFA构造所需参数
*/
public ElementNFA constructNFA(SuffixExpression se) {}
/**
* NFA使用|操作进行拼接
*
* @param elementNFA1 NFA1的构造所需参数
* @param elementNFA2 NFA2的构造所需参数
* @return 返回NFA1与NFA2结合后NFA构造所需参数用以继续进行结合操作或构造NFA
*/
protected ElementNFA combineNFA(ElementNFA elementNFA1, ElementNFA elementNFA2) {}
/**
* NFA构造算法
*
* @param vertexes 图的节点
* @param edges 图的边
* @param matchTypes 匹配类型,子集构造算法将会传递此参数
* @param enterChars 输入字符集合
*/
protected NFA(NFA_Node[] vertexes, String[][] edges, MatchTypeNFA[] matchTypes, char[] enterChars) {}
/**
* 打印表示NFA图的邻接表
*/
protected void print(LinkedList<NFA_Node>[] linkedLists) {}
/**
* 子集构造算法
*
* @return 返回构造后的DFA
*/
protected DFA subsetConstruct() {}
/**
* 调用此方法对DFA进行最小化
*/
protected void minimizeDFA() {}
//构造Constructor
public TableDriven(DFA dfa) {}
/**
* 读取文件C语言代码,并执行表驱动程序
*
* @param fileAddress 文件路径
* @throws IOException 读取操作失败抛出
*/
protected void readC(String fileAddress) throws IOException {}
调试过程和运行结果
总运行结果
表驱动所得二元组如下:
( KEY_WORDS , void )
( ID , test )
( KEY_SYMBOLS , ( )
( KEY_WORDS , int )
( ID , a )
( KEY_SYMBOLS , , )
( KEY_WORDS , int )
( ID , b )
( KEY_SYMBOLS , , )
( KEY_WORDS , float )
( ID , c )
( KEY_SYMBOLS , , )
( KEY_WORDS , float )
( ID , d )
( KEY_SYMBOLS , ) )
( KEY_SYMBOLS , { )
( KEY_WORDS , int )
( ID , x )
( KEY_SYMBOLS , ; )
( KEY_WORDS , float )
( ID , y )
( KEY_SYMBOLS , ; )
( ID , x )
( ASSIGN , = )
( ID , a )
( KEY_SYMBOLS , + )
( ID , b )
( KEY_SYMBOLS , ; )
( ID , y )
( ASSIGN , = )
( ID , c )
( KEY_SYMBOLS , / )
( ID , d )
( KEY_SYMBOLS , ; )
( KEY_WORDS , if )
( KEY_SYMBOLS , ( )
( ID , x )
( RELATION , > )
( ID , y )
( KEY_SYMBOLS , ) )
( ID , x )
( ASSIGN , = )
( DIGITS , 10 )
( KEY_SYMBOLS , ; )
( KEY_WORDS , else )
( ID , y )
( ASSIGN , = )
( DECIMAL , 12.5 )
( KEY_SYMBOLS , ; )
( KEY_SYMBOLS , } )
手动构建DFA进行表驱动算法测试
测试内容
使用自己创建的DFA测试ID、DEMICA、DIGIT三种类型的词汇,测试内容为老师提供的C语言代码
/**
* 表驱动算法测试
*
* @throws IOException 读取文件异常
*/
@org.junit.jupiter.api.Test
void readC() throws IOException {
//测试DFA数据结构
//手动构建DFA
DFA_Node[] vexes = {new DFA_Node('A'), new DFA_Node('B', '#'), new DFA_Node('C'), new DFA_Node('D', '#'), new DFA_Node('E', '#')};
char[][] edges = new char[][]{
{'A', 'B', 'a'},
{'A', 'B', 'b'},
{'A', 'B', 'c'},
/*-------------------------------------中间部分省略------------------------------------*/
{'E', 'E', '5'},
{'E', 'E', '6'},
{'E', 'E', '7'},
{'E', 'E', '8'},
{'E', 'E', '9'}
};
MatchTypeDFA[] matchTypes = {new MatchTypeDFA('B', "ID"), new MatchTypeDFA('D', "DIGITS"), new MatchTypeDFA('E', "DECIMAL")};
DFA dfa = new DFA(vexes, edges, matchTypes);
dfa.print(dfa.vertexLists); // 打印图
TableDriven tableDriven = new TableDriven(dfa);
tableDriven.readC("./Cproject.c");
}
测试结果
当前DFA邻接表如下,带“#”符号代表为终结状态
A --a-> B --b-> B --c-> B --d-> B --e-> B --f-> B --g-> B --h-> B --i-> B --j-> B --k-> B --l-> B --m-> B --n-> B --o-> B --p-> B --q-> B --r-> B --s-> B --t-> B --u-> B --v-> B --w-> B --x-> B --y-> B --z-> B --0-> D --1-> D --2-> D --3-> D --4-> D --5-> D --6-> D --7-> D --8-> D --9-> D
B(#) --a-> B --b-> B --c-> B --d-> B --e-> B --f-> B --g-> B --h-> B --i-> B --j-> B --k-> B --l-> B --m-> B --n-> B --o-> B --p-> B --q-> B --r-> B --s-> B --t-> B --u-> B --v-> B --w-> B --x-> B --y-> B --z-> B --0-> B --1-> B --2-> B --3-> B --4-> B --5-> B --6-> B --7-> B --8-> B --9-> B --_-> C
C --a-> B --b-> B --c-> B --d-> B --e-> B --f-> B --g-> B --h-> B --i-> B --j-> B --k-> B --l-> B --m-> B --n-> B --o-> B --p-> B --q-> B --r-> B --s-> B --t-> B --u-> B --v-> B --w-> B --x-> B --y-> B --z-> B --0-> B --1-> B --2-> B --3-> B --4-> B --5-> B --6-> B --7-> B --8-> B --9-> B
D(#) --0-> D --1-> D --2-> D --3-> D --4-> D --5-> D --6-> D --7-> D --8-> D --9-> D --.-> E
E(#) --0-> E --1-> E --2-> E --3-> E --4-> E --5-> E --6-> E --7-> E --8-> E --9-> E
表驱动所得二元组如下:
( KEY_WORDS , void )
( ID , test )
( KEY_SYMBOLS , ( )
( KEY_WORDS , int )
( ID , a )
( KEY_SYMBOLS , , )
( KEY_WORDS , int )
( ID , b )
( KEY_SYMBOLS , , )
( KEY_WORDS , float )
( ID , c )
( KEY_SYMBOLS , , )
( KEY_WORDS , float )
( ID , d )
( KEY_SYMBOLS , ) )
( KEY_SYMBOLS , { )
( KEY_WORDS , int )
( ID , x )
( KEY_SYMBOLS , ; )
( KEY_WORDS , float )
( ID , y )
( KEY_SYMBOLS , ; )
( ID , x )
( ASSIGN , = )
( ID , a )
( KEY_SYMBOLS , + )
( ID , b )
( KEY_SYMBOLS , ; )
( ID , y )
( ASSIGN , = )
( ID , c )
( KEY_SYMBOLS , / )
( ID , d )
( KEY_SYMBOLS , ; )
( KEY_WORDS , if )
( KEY_SYMBOLS , ( )
( ID , x )
( RELATION , > )
( ID , y )
( KEY_SYMBOLS , ) )
( ID , x )
( ASSIGN , = )
( DIGITS , 10 )
( KEY_SYMBOLS , ; )
( KEY_WORDS , else )
( ID , y )
( ASSIGN , = )
( DECIMAL , 12.5 )
( KEY_SYMBOLS , ; )
( KEY_SYMBOLS , } )
手动构建非最小DFA进行DFA最小化算法测试
测试内容
使用ppt上的题目进行测试
/**
* 最小化DFA算法测试1
*/
@org.junit.jupiter.api.Test
void minimizeDFA1() {
//DFA最小化算法测试
DFA_Node[] vexes = {new DFA_Node('A'), new DFA_Node('B'), new DFA_Node('C'), new DFA_Node('D'), new DFA_Node('E', '#')};
char[][] edges = new char[][]{
{'A', 'B', 'a'},
{'A', 'C', 'b'},
{'B', 'B', 'a'},
{'B', 'D', 'b'},
{'C', 'B', 'a'},
{'C', 'C', 'b'},
{'D', 'B', 'a'},
{'D', 'E', 'b'},
{'E', 'B', 'a'},
{'E', 'C', 'b'}
};
MatchTypeDFA[] matchTypes = {new MatchTypeDFA('E', "ID")};
DFA dfa = new DFA(vexes, edges, matchTypes);
dfa.print(dfa.vertexLists);
dfa.minimizeDFA();
}
测试结果
当前DFA邻接表如下,带“#”符号代表为终结状态
A --a-> B --b-> C
B --a-> B --b-> D
C --a-> B --b-> C
D --a-> B --b-> E
E(#) --a-> B --b-> C
执行最小化划分:
_ _ _ _ E
A B C D _
---split---
_ _ _ _ E
A B C _ _
_ _ _ D _
---split---
--------SplitAgain--------
_ _ _ _ E
A _ C _ _
_ B _ D _
---split---
_ _ _ _ E
A _ C _ _
_ _ _ D _
_ B _ _ _
---split---
--------SplitAgain--------
经过划分,当前DFA邻接表如下,带“#”符号代表为终结状态
A --a-> A --b-> B
B --a-> A --b-> D
C --a-> A --b-> C
D(#) --a-> A --b-> C
手动构造NFA进行子集构造算法测试
测试内容
使用ppt上的题目进行测试
/**
* 子集构造算法测试
*/
@org.junit.jupiter.api.Test
void subsetConstruct() {
//测试NFA数据结构
//手动构建NFA
NFA_Node[] vexes = {new NFA_Node(0), new NFA_Node(1), new NFA_Node(2), new NFA_Node(3), new NFA_Node(4), new NFA_Node(5), new NFA_Node(6), new NFA_Node(7), new NFA_Node(8), new NFA_Node(9), new NFA_Node(10, '#')};
char[] enterChars = {'a', 'b'};
String[][] edges = new String[][]{
{"0", "1", "ε"},
{"0", "7", "ε"},
{"1", "2", "ε"},
{"1", "4", "ε"},
{"2", "3", "a"},
{"3", "6", "ε"},
{"4", "5", "b"},
{"5", "6", "ε"},
{"6", "1", "ε"},
{"6", "7", "ε"},
{"7", "8", "a"},
{"8", "9", "b"},
{"9", "10", "b"}
};
MatchTypeNFA[] matchTypes = {new MatchTypeNFA(10, "ID")};
NFA nfa = new NFA(vexes, edges, matchTypes, enterChars);
nfa.print(nfa.vertexLists); // 打印图
DFA dfa = nfa.subsetConstruct();
//dfa.minimizeDFA();
}
测试结果
当前NFA邻接表如下,带“#”符号代表为终结状态
0 --ε-> 1 --ε-> 7
1 --ε-> 2 --ε-> 4
2 --a-> 3
3 --ε-> 6
4 --b-> 5
5 --ε-> 6
6 --ε-> 1 --ε-> 7
7 --a-> 8
8 --b-> 9
9 --b-> 10
10(#)
子集构造算法流程如下:
ε-闭包({0}) = A | 0 1 7 2 4
ε-闭包(smove( A , a ))= B | 8 3 6 1 7 2 4
ε-闭包(smove( A , b ))= C | 5 6 1 7 2 4
ε-闭包(smove( B , a ))= B |
ε-闭包(smove( B , b ))= D | 9 5 6 1 7 2 4
ε-闭包(smove( C , a ))= B |
ε-闭包(smove( C , b ))= C |
ε-闭包(smove( D , a ))= B |
ε-闭包(smove( D , b ))= E | 10 5 6 1 7 2 4
ε-闭包(smove( E , a ))= B |
ε-闭包(smove( E , b ))= C |
经子集构造,得到DFA,当前DFA邻接表如下,带“#”符号代表为终结状态
A --a-> B --b-> C
B --a-> B --b-> D
C --a-> B --b-> C
D --a-> B --b-> E
E(#) --a-> B --b-> C
输入正规式进行Thompson算法构造NFA测试
输入单个正规式测试
测试内容
使用ppt上的题目进行测试
/**
* Thompson算法测试1
*/
@org.junit.jupiter.api.Test
void thompsonTest1() {
//正规式
RegularExpression[] res = new RegularExpression[]{new RegularExpression("(a|b)*abb", "TEST")};
//将正规式转为后缀正规式
SuffixExpression[] ses = new SuffixExpression[res.length];
for (int i = 0; i < res.length; i++) {
ses[i] = new SuffixExpression(res[i]);
}
//构造Thompson类对象
Thompson thompson = new Thompson();
//组合所需NFA元素
ElementNFA elementNFA = thompson.constructNFA(ses[0]);
for (int i = 1; i < ses.length; i++) {
elementNFA = thompson.combineNFA(elementNFA, thompson.constructNFA(ses[i]));
}
//生成NFA
NFA nfa = new NFA(elementNFA.vertexes, elementNFA.edges, elementNFA.matchTypes, elementNFA.enterChars);
nfa.print(nfa.vertexLists);
}
测试结果
原正规式: (a|b)*abb 经处理得: (a|b)*&a&b&b
中缀正规式: (a|b)*&a&b&b 转为后缀正规式: ab|*a&b&b&
当前NFA邻接表如下,带“#”符号代表为终结状态
0 --ε-> 4 --ε-> 7
1 --ε-> 5
2 --b-> 3
3 --ε-> 5
4 --ε-> 6 --ε-> 2
5 --ε-> 4 --ε-> 7
6 --a-> 1
7 --a-> 8
8 --b-> 9
9 --b-> 10
10(#)
输入多个正规式测试
测试内容
/**
* Thompson算法测试2
*/
@org.junit.jupiter.api.Test
void thompsonTest2() {
//正规式
RegularExpression[] res = new RegularExpression[]{new RegularExpression("abc", "TEST1"), new RegularExpression("cba", "TEST2")};
//将正规式转为后缀正规式
SuffixExpression[] ses = new SuffixExpression[res.length];
for (int i = 0; i < res.length; i++) {
ses[i] = new SuffixExpression(res[i]);
}
//构造Thompson类对象
Thompson thompson = new Thompson();
//组合所需NFA元素
ElementNFA elementNFA = thompson.constructNFA(ses[0]);
for (int i = 1; i < ses.length; i++) {
elementNFA = thompson.combineNFA(elementNFA, thompson.constructNFA(ses[i]));
}
//生成NFA
NFA nfa = new NFA(elementNFA.vertexes, elementNFA.edges, elementNFA.matchTypes, elementNFA.enterChars);
nfa.print(nfa.vertexLists);
}
测试结果
原正规式: abc 经处理得: a&b&c
原正规式: cba 经处理得: c&b&a
中缀正规式: a&b&c 转为后缀正规式: ab&c&
中缀正规式: c&b&a 转为后缀正规式: cb&a&
当前NFA邻接表如下,带“#”符号代表为终结状态
0 --ε-> 1 --ε-> 5
1 --a-> 2
2 --b-> 3
3 --c-> 4
4(#) --ε-> 9
5 --c-> 6
6 --b-> 7
7 --a-> 8
8(#) --ε-> 9
9
程序的主要部分及其功能说明
表驱动算法(TableDriven.java)
-
定义保留词,此部分参考了老师给的提示文档及ppt定义了形如
private final String[] KEY_WORDS = {"void", "main", "int", "float", "char", "if", "else", "while", "for", "return"};的保留词数组用于匹配算法,亦可使用HashMap提升效率。若将关键词以正规式方式进行输入亦可,但是要注意特殊处理字符转义
-
接收经过最小化处理的DFA(邻接表图结构)
-
逐行读取文件内容,按空白分段处理
-
定义StringBuilder存储已输入且匹配DFA的字符,识别最长部分,优先比较保留词,若无法匹配保留词则按照DFA终态对应类型输出二元组
-
若存在单个字符无法在DFA中被匹配,将其抛弃,若出现连续多个字符无法匹配则讲整个分段以错误二元组输出
DFA最小化算法(DFA.java)
- 此部分除了最小化算法还包含了DFA的数据结构定义和构造方法
- 最小化部分我的做法是维护一个二维数组(长度即为状态数)
- 将状态以是否终态分为两个组(后被发现是不必要的,但是契合ppt观点,我将其保留了),每一行(及一个划分组)遍历所有状态,若该状态有可合并状态则不将其分除,否则将其下移至下一划分组
- 反复执行3中操作,知道所有划分组无需继续划分,则划分完成
- 将划分组中的元素合并,更改一些细节重构一个DFA完成最小化
子集构造算法(NFA.java)
- 此部分除了子集构造算法还包含了NFA的数据结构定义和构造方法
- 使用smove操作反复求取分组,将求取分组过程中的参数转为DFA构造所需参数
- 此算法为本次实验中较简单的一个,主要精力花在了反复修改的NFA和DFA的数据结构上
Thompson算法(TableDriven.java)
-
此部分除了Thompson算法还包含了正规式的数据结构定义和构造方法
-
将构造好的正规式改造为后缀正规式(参考了后缀表达式的构造),考虑到的情况较为简单,只有:&、|、-、(、)、* 这六种操作符,已经足够满足本次实验要求,本次C语言代码匹配我构造的内容如下
原正规式(DIGIT): (0-9)(0-9)* 经处理得: (0|1|2|3|4|5|6|7|8|9)&(0|1|2|3|4|5|6|7|8|9)* 原正规式(DECIMAL): (0-9)(0-9)*.(0-9)(0-9)* 经处理得: (0|1|2|3|4|5|6|7|8|9)&(0|1|2|3|4|5|6|7|8|9)*&.&(0|1|2|3|4|5|6|7|8|9)&(0|1|2|3|4|5|6|7|8|9)* 原正规式(ID): (a-z)(a-z)*(_(a-z)(a-z)*)* 经处理得: (a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)&(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)*&(_&(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)&(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x|y|z)*)* -
将后缀正规式借用栈进行操作(参考后缀表达式求值),构造DFA节点及DFA边,最终构造完整NFA并返回
-
NFA合并算法:若有多个NFA,需要用到Thompson算法中的|操作进行合并
实验收获体会
- 深刻理解了词法遍历器构建流程
- 复习了正规式形如中/后缀表达式的栈结构运用
- 学习到提前设计需要使用的通用结构的重要性
- 写算法时一些自写的查询用到了二分或其他算法节省时间、空间复杂度,练习了算法
改进意见
- 因为是从表驱动写起,前期没有太大数据量没有注意节约时间与空间,后期Thompson算法生成的NFA及其所构造的DFA内容较多跑程序慢,改进时要多优化小方法的时间及空间复杂度,以避免多次调用时浪费过多时间与空间
- 输出内容中无用占位符过多,当规模较大时,会影响查看输出(大部分都是无用占位符)。改进时可更改数据结构使用空间复杂度较小的结构并且添加判断再进行输出
- 表驱动和DFA构造最小化两部分可读性较差,循环过多,即使添加了较多注释依然读的很费力,改进时应做一定拆分,封装好方法。
项目代码
链接: pan.baidu.com/s/1e7EeblHb… 提取码: 5qr6