自制脚本语言(4) 自动生成的词法分析器
2015-09-28 14:34:48 nklofy 阅读数 1859更多
分类专栏: 自制脚本语言
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/nklofy/article/details/46809617
摘要:设计并实现了词法分析器。读取文件中的正则表达式及其相匹配的符号,生成由NFA到DFA转移表,最终得到表格驱动的词法分析器。
词法分析器是根据一个符号表分析文本文档。符号表中有两种符号,一种是保留字,例如if、else这类,还有一种是正则表达式,例如整型和浮点型数字、普通的变量名等。词法分析器的功能就是读取原始的文本文档,将其按照符号定义分割为符号的序列。那么对外提供一个接口getToken(),每次调用,返回一个已分析的符号。整个文档分析完成后,需要有个“EOF”符号。
getToken( )的过程是读取字符,与DFA确定型有限自动机进行匹配,当遇到空格或换行时,自动机刚好是结束状态,则返回相应的符号。这里面有几个要注意的点。通常来讲,这个过程是按照正则表达式,先构建NFA,然后生成DFA。但有几个点稍微要注意。一是正则表达式无非是数字、变量名,保留字、运算符都是确定的字符串。那么,对确定的字符串就没必要生成NFA,可以直接生成DFA。二是运算符往往跟数字和变量名、保留字之间都没有空格分开。那么,就要自动将运算符和数字、变量名、保留字分割。
这里设计的几个类,首先最基本的表示自动机的NFA_State与DFA_State。其次要有表示正则表达式的RegexPattern与普通保留字的ReservedWord。最后,有个RegexParser来分析正则表达式生成NFA。
-
class NFA_State:
-
HashMap<Character,HashSet<NFA_State>> nfa_edges //字符转移的路径
-
HashSet<NFA_State> e_edges //e转移的路径
-
class DFA_State:
-
HashMap<Character,DFA_State> dfa_edges //字符转移的路径
-
class RegexPaser:
-
String rule; //要解析的正则表达式String
-
NFA_State parse() //对外的解析接口
-
NFA_State parsePar() //解析()*等科林闭包形式
-
NFA_State parseSqr() //解析[]多选一形式
-
NFA_State parseDsj() //解析 | 选择形式
-
NFA_State parseSeq() //解析连续的字符形式
NFA有两种边,一种是e转移,一种是根据字符的转移,对应伪代码里面的Map和Set。DFA只有根据字符的转移,对应Map。RegexParser对外的接口是parse(string rule),而内部需要根据正则表达式的()*、[ ]*、| 等符号,解析出科林闭包、选择关系等。其实也可以归纳到BNF范式然后用递归下降来写,也就是说把正则表达式转换成抽象语法树。但是现在不需要支持太复杂的正则表达式,所以就用了一个简单的“面条式”写法,把符号解析写到switch...case...过程里面。
程序运行,读取文件,获得全部RegexPattern与ReservedWord对象不提,置入两个ArrayList表格table_pt与table_rs。然后先分析ReservedWord,生成DFA。分析每一条ReservedWord,从全局变量dfa_start开始,对照保留字的每一个字符,查找是否有相同字符的路径,如果没有,则加入新路径;否则移动到下一状态,继续分析保留字的下一个字符。
generateDFA()
再生成NFA。这里解析正则表达式没有生成抽象语法树,而是直接构建了DFA。每个字符代表的NFA状态前后都有一个e转移,记为pre与crt,遇到( )*科林闭包,crt与pre之间添加e转移边。而遇到[ ]或者 | 符号选择式,在pre与crt之间插入多条字符路径。
generateNFA()
有了NFA,再将NFA转为DFA。这里用的算法是子集构造法。从全局变量nfa_start开始,寻找e-closure。获得NFA子集后,查明如果是新的子集,则生成一个新的DFA,将这个DFA标为all_start。在NFA子集中,发现全部的字符路径,对每条路径,给all_start加入一条新的边,其DFA终点为原NFA集合中此字符路径终点的全部NFA的子集对应的DFA。所以,这里我们需要根据NFA子集查找DFA,以及根据DFA查找原NFA子集的两个表。现在暂时这两个表都用HashSet,但因为查找子集实际上不是判断子集的Set是否一致而是判断Set的元素是否一致,所以Hash其实是没有必要的。未来优化会用TreeSet,便于查找。还有一个问题,关于终结状态。NFA的终结态实际是所有可以e转移到终结态的状态,而没有本身标记为终结态。那么在前面的e-closure过程,子集包括了终结态的NFA子集,对应生成的DFA也应该标记为Final终结态。
-
NFAtoDFA() //调用spreadDFA()过程,将NFA转为DFA
-
spreadDFA() //递归调用。从一个NFA开始,获得e-closure子集,再得到所有字符路径,生成对应的DFA加入起始DFA路径中
-
getEClosure() //由NFA子集开始,e转移得到新的NFA子集,此子集或者可以e转移到终结态,或者可以字符路径转移
-
getEdges() //由NFA子集开始,分析其全体字符路径,获得一个Map表示不同字符转移到不同的新DFA
-
combineDFA() //合并两个DFA
-
getTokenTable() //输出转移表
最后要把NFA转换的DFA与开始分析保留字的DFA合并起来。方法类似于分析保留字,边深度遍历DFA,边判断是否需要加入新的边与新的DFA。分析合并后的DFA,最终得到一个转移表,根据字符转移状态,到达某种终结态时,如果符合终结条件(例如空格或者换行,或者数字、字母与运算符operator的切换),则确认完成一个token的读取,字符数组作为buffer输出,根据终结态判断token的分类、名字、数值等属性,送到parser处理。
如此则完成了可处理正则表达式的词法分析器,当然,这是没有优化的最基础版本,以后会找时间改进。要改进的地方包括支持更复杂的正则表达式,有可能用抽象语法树来处理,NFA的子集的快速查找,可能用TreeSet或者数字编号索引的办法,还有NFA转DFA的算法是否可以提高,对集合的运算是否可以写成模板方法或者自动机的模板类,还可以考虑做一个在文本中查找匹配的正则引擎,等等等等。还有继续完成语法分析Parser、解释器以及编译器、虚拟机。
源代码地址:https://github.com/nklofy/Compiler