自制脚本语言(4) 自动生成的词法分析器

自制脚本语言(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。

 

 
  1. class NFA_State:

  2. HashMap<Character,HashSet<NFA_State>> nfa_edges //字符转移的路径

  3. HashSet<NFA_State> e_edges //e转移的路径

  4. class DFA_State:

  5. HashMap<Character,DFA_State> dfa_edges //字符转移的路径

  6. class RegexPaser:

  7. String rule; //要解析的正则表达式String

  8. NFA_State parse() //对外的解析接口

  9. NFA_State parsePar() //解析()*等科林闭包形式

  10. NFA_State parseSqr() //解析[]多选一形式

  11. NFA_State parseDsj() //解析 | 选择形式

  12. 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终结态。

 
  1. NFAtoDFA() //调用spreadDFA()过程,将NFA转为DFA

  2. spreadDFA() //递归调用。从一个NFA开始,获得e-closure子集,再得到所有字符路径,生成对应的DFA加入起始DFA路径中

  3. getEClosure() //由NFA子集开始,e转移得到新的NFA子集,此子集或者可以e转移到终结态,或者可以字符路径转移

  4. getEdges() //由NFA子集开始,分析其全体字符路径,获得一个Map表示不同字符转移到不同的新DFA

 
  1. combineDFA() //合并两个DFA

  2. getTokenTable() //输出转移表

  最后要把NFA转换的DFA与开始分析保留字的DFA合并起来。方法类似于分析保留字,边深度遍历DFA,边判断是否需要加入新的边与新的DFA。分析合并后的DFA,最终得到一个转移表,根据字符转移状态,到达某种终结态时,如果符合终结条件(例如空格或者换行,或者数字、字母与运算符operator的切换),则确认完成一个token的读取,字符数组作为buffer输出,根据终结态判断token的分类、名字、数值等属性,送到parser处理。

 

  如此则完成了可处理正则表达式的词法分析器,当然,这是没有优化的最基础版本,以后会找时间改进。要改进的地方包括支持更复杂的正则表达式,有可能用抽象语法树来处理,NFA的子集的快速查找,可能用TreeSet或者数字编号索引的办法,还有NFA转DFA的算法是否可以提高,对集合的运算是否可以写成模板方法或者自动机的模板类,还可以考虑做一个在文本中查找匹配的正则引擎,等等等等。还有继续完成语法分析Parser、解释器以及编译器、虚拟机。

源代码地址:https://github.com/nklofy/Compiler

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实验二 词法分析器 一、实验目的 掌握词法分析器的构造原理,掌握手工编程或LEX编程方法之一。 二、实验内容 编写一个LEX源程序,使之生成一个词法分析器,能够输入的源程序转换为单词序列输出。 三、实验环境 Flex+VC6.0 四、实验注意 1.Id正则表达式:{letter}({letter}|{digit})* 2.Num正则表达式:{digit}+(\.{digit}+)?(E[+-]?{digit}+)? 3.注释:(\/\*(.)*\*\/) 4.关键字再加上其他字符就又能编程id,所以在词法分析时,id的判断应该放在关键字前面,这样才不会误判 5.由于本程序知识简单的打印数字,因此没有考虑数字的转换 6.">="比">"多一个字符,它应该放在前面判断,其他类似的也应该如此安排 五、实验代码 ******************************************************************************* 实验文件:lex.l、lex.yy.c 实验结果:lex.exe 运行方式:打开lex.exe,弹出input.txt,在其中输入所要测试的程序,保存并关闭,即可在output.txt中看到所得结果 ******************************************************************************* %{ void Install(char *type); %} %option noyywrap delim [ \t] newline [\n] digit [0-9] num {digit}+(\.{digit}+)?(E[+-]?{digit}+)? letter [A-Za-z] id {letter}({letter}|{digit})* key ("if"|"while"|"do"|"break"|"true") basic ("int"|"float"|"bool"|"char") op (">="|""|"<"|"="|"!="|"+"|"-"|"*"|"/") comment (\/\*(.)*\*\/) %% delim {;} newline {printf("\n");} {num} {Install("Num");} {key} {Install("Key");} {basic} {Install("Basic");} {op} {Install("Op");} ";" {Install("Comma");} {id} {Install("ID");} {comment} {Install("Comment");} "(" | "[" | "{" {Install("lbracket");} ")" | "]" | "}" {Install("rbracket");} %% void Install(char *s) { fprintf(yyout, "%s:%s ", s, yytext); } int main() { printf("please input the test program in input.txt\n"); system("input.txt"); yyin = fopen("input.txt", "r"); yyout = fopen("output.txt", "w" ); yylex(); fclose(yyout); fclose(yyin); printf("analysis result in output.txt\n"); system("output.txt"); return 0; } 六、实验小结 本次的实验由于使用了flex,所以代码较短,麻烦的事flex的正则式表达,由于该使用规则只有简单介绍,而网上找的教程难免有比重就轻之嫌,所以得到上述表达式着实费力,且有的没有成功,例如bracket的(\ ((.)*\ ))或者("("(.)*")")使用时都没有成功,所以便单独写出,有点不伦不类。至于其他的,都较为简单,完。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值