系列导航html
在以前的六篇文章中,我比较详细的介绍了与词法分析器相关的算法。它们都比较关注于实现的细节,感受上可能比较凌乱,本篇就从总体上介绍一下如何定义词法分析器,以及如何实现本身的词法分析器。git
第二节完整的介绍了如何定义词法分析器,能够看成一个词法分析器使用指南。若是不关心词法分析器的具体实现的话,能够只看第二节。github
1、类库的改变
首先须要说明一下我对类库作的一些修改。词法分析部分的接口,与当初写《C# 词法分析器》系列时相比,已经发生了不小的改变,有必要作一下说明。正则表达式
1. 词法单元的标识符
词法单元(token)最初的定义是一个 Token 结构,使用一个 int 属性做为词法单元的标识符,这也是不少词法分析器的通用作法。算法
但后来作语法分析的时候,感受这样很是不方便。由于目前还不支持从定义文件生成词法分析器代码,只能在程序里面定义词法分析器。而 int 自己是不具备语义的,做为词法单元的标识符来使用,不但不方便还容易出错。数组
后来尝试过使用字符串做为标识符,虽然解决了语义的问题,但仍然容易出错,实现上也会复杂些(须要保存字符串字典)。缓存
而既简单,又具备语义的解决方案,就是使用枚举了。枚举名称提供了语义,枚举值又能够转换为整数,并且还可以提供编译期检查,彻底避免了拼写错误,因此如今的词法单元便定义为 Token<T> 类,与之相关的不少类也一样带上了泛型参数 T。post
2. 命名空间
以前的命名空间是 Cyjb.Compiler 和 Cyjb.Compiler.Lexer,如今被改为了 Cyjb.Compilers 和 Cyjb.Compilers.Lexers,毕竟命名空间名称仍是比较适合使用复数。htm
3. 词法分析器上下文
以前对词法分析器上下文的切换,可使用上下文的索引、标签或 LexerContext 实例自己。但如今只可以经过标签进行切换,这样实现起来更简单些,使用上也不会受到过多影响。对象
4. DFA 的表示
原先 LexerRule 类中对 DFA 的表示有些简单粗暴,对于不了解具体实现的人来讲,很难理解 DFA 的表示。如今从新规划了 LexerRule<T> 类中的接口,理解起来会更容易些。
2、定义词法分析器
这一节是 Cyjb.Compilers 类库中词法分析器的使用指南,包含了完整的文档、实例以及相关注意事项。类库的源码能够从 Cyjb.Compilers 项目找到,类库文档请参见 wiki。
1. 定义词法单元的标识符
前面说到,目前是使用枚举类型做为词法单元的标识符,这个枚举类型中的字段能够任意定义,没有任何限制。不过,为了方便以后的语法分析部分,特别要求枚举值必须是从 0 开始的整数,枚举值最好是连续的,由于不连续的枚举值会致使语法分析部分浪费更多的空间。
使用特殊的值 -1 来表示文件结束(EndOfFile),该值能够从 Token<T>.EndOfFile 字段获得,也能够经过 Token<T>.IsEndOfFile 属性获取词法单元是否表示文件结束。
这里仍然使用计算器做为示例,如下代码便定义了做为标识符的枚举:
在使用的时候,显然会比整数更加方便。
2. 定义词法分析器的上下文
词法分析器的全部定义都是从 Cyjb.Compilers.Grammar<T> 类开始的,所以首先须要实例化一个 Grammar<T> 类的实例:
词法分析器的上下文,能够用来控制规则是否生效。上下文有两种类型:包含型或者排除型。
- 若是当前是包含型上下文,那么会激活当前上下文的全部规则,同时会激活全部没有指定任何上下文的规则。
- 若是当前是排除型上下文,那么只会激活当前上下文的全部规则,其它任何规则都不会被激活。
使用如下的方法来分别定义排除型和包含型的词法分析器上下文,label 参数即为上下文的标签:
默认的词法分析器上下文是 "Initial",经过该标签能够切换到默认的上下文中。须要特别注意的是,因为实现上的缘由,上下文必须先于全部终结符定义。
例如,如下的代码定义了一个包含型上下文 Inc,以及一个排除型上下文 Exc。
3. 定义正则表达式
使用如下的方法来定义正则表达式:
正则表达式能够经过 Cyjb.Compilers.RegularExpressions.Regex 类的相关方法构造获得,也能够直接使用表示正则表达式的字符串,相关定义的规则能够参考 《C# 词法分析器(三)正则表达式》。
注意,这里定义的正则表达式仅仅用于简化终结符定义,方便重复使用一些通用或复杂的正则表达式,并无其它的做用。这里定义的正则表达式也不能够包含向前看符号(/)、行首限定符(^)、行尾限定符($)或者上下文(<context>)。
例如,如下代码定义了一个名为 digit 的正则表达式,之后须要表示数字的时候,就能够直接经过 “{digit}” 来引入,而不须要每次都写 “[0-9]+”。
4. 定义终结符
使用 Grammar<T>.DefineSymbol 方法的相关重载来定义终结符,如如下代码所示:
这些重载被分红了三组。第一组重载,接受 T id 做为与词法单元对应的标识符,和相应的正则表达式及其上下文。当相应的终结符被匹配后,自动返回标识符为 id 的 Token<T>实例。
第二组重载,具备额外的参数 action,这是只包含一个 ReaderController<T> 参数的委托,当匹配了相应的终结符时,就会被调用。经过 ReaderController<T> 的相应属性和方法,能够对词法分析过程进行一些控制。
最后一组重载,缺乏了标识符 id,也就没法自动返回 Token<T> 实例,所以必须指定匹配到相应终结符时要执行的方法。
终结符的动做
在成功匹配某个终结符时,就会执行相应的动做,该动做是一个 Action<ReaderController<T>> 类型的委托。
ReaderController<T> 类包含了与当前匹配的终结符相关的信息,包括上下文、标识符、源文件和文本。主要的方法有 Accept、More、Reject 以及操纵上下文的方法 BeginContext、PopContext 和 PushContext。
Accept 方法会接受当前的匹配,词法分析器会返回表示当前匹配的 Token<T> 实例。
More 方法会通知词法分析器,保留本次匹配的文本。假设本次匹配的文本是 "foo",下次匹配的文本是 "bar",若是本次匹配时调用了 More 方法,下次匹配的文本就会变成 "foobar"。
Reject 方法会拒绝当前的匹配,转而使用次优的规则继续尝试匹配。详细信息请参考《C# 词法分析器(六)构造词法分析器》的 2.4 节“支持 Reject 动过的词法分析器”。
Accept 方法和 Reject 方法不可以在一次匹配中同时调用,由于它们是互斥的动做。若是在一次匹配中两个方法都没有调用,那么词法分析器会什么都不作——丢弃本次匹配的结果,直接进行下一次匹配。
对于词法分析器上下文的控制,简单的用法就是利用上下文来切换匹配的规则集,以实现一些“次级语法”,能够参考《C# 词法分析器(六)构造词法分析器》的 3.3 节给出的示例“转义的字符串”。
下面给出计算器的终结符定义。其中,Id 的定义是经过引入正则表达式 digit 来完成的,并且它定义了本身的动做,会将本身对应的文本转换为 double 类型,并保存到 Token<T>.Value 属性中。最后一条语句,经过定义空的动做,使得匹配到的空白被丢弃。
5. 构造词法分析器
以上四步便完成了词法分析器的定义,接下来就是构造词法分析器。使用如下四个方法,就能够直接构造出相应的词法单元读取器(TokenReader<T> 的子类的实例):
若是调用的是 GetReader 方法重载,则认为动做中不包含拒绝(Reject),会返回比较简单但更高效的词法分析器实现。若是调用的是 GetRejectableReader 方法重载,则认为动过中包含拒绝(Reject),会返回功能更强大但效率略低的词法分析器实现。
其规则是:
- 若是不包含向前看和拒绝动做,则返回 SimpleReader<T> 的实例。
- 若是只包含定长的向前看(不包含变长的向前看或拒绝动做),则返回 FixedTrailingReader<T> 的实例。
- 若是只包含拒绝动做(不包含向前看),则返回 RejectableReader<T> 的实例。
- 若是包含变长的向前看,或者同时包含拒绝动做和向前看(不管是否变长),则返回 RejectableTrailingReader<T> 的实例。
关于其中实现的细节,请参考《C# 词法分析器(六)构造词法分析器》。
全部的词法单元读取器,都继承自 TokenReader<T> 类,主要包含两个方法:PeekToken 和 ReadToken,与字面意义相同,就是读取输入流中的下一个词法单元,不更改(Peek)或提高(Read)输入流的字符位置。
TokenReader<T> 类还实现了 IEnumerable<T> 接口,所以可使用 foreach 语句从中读取词法单元。可是,TokenReader<T> 自己并不会储存以前读取过的词法单元,在被枚举的时候,实际上仍是会调用 ReadToken 方法,所以只能在一个位置枚举 TokenReader<T>,并且只能枚举一次,枚举完毕后,TokenReader<T> 也一样到达了流的结尾。若是但愿屡次枚举,还请缓存到数组中再进行操做。
如下是构造出计算器的词法单元读取器,并输出全部读取到的词法单元的代码:
最后是完整的构造计算器的代码:
代码的输出结果以下图所示:
能够看到,最后老是会以特殊值 -1 结束,表示文件结束。
3、自定义词法分析器
Cyjb.Compilers 项目中,提供了完整的词法分析器实现。可是,在实际的使用中,不免会遇到各类各样的需求,可能已实现的词法分析器是没法知足的,此时就必须本身完成词法分析器了。
在完成定义词法分析器后,能够从 Grammar<T>.LexerRule 属性获取到一个 Cyjb.Compilers.Lexers.LexerRule<T> 对象,该实例中存储了一个词法分析器所需的所有信息,而且不会依赖于原始的 Grammar<T> 对象。它就是自定义词法分析器的核心。
下图是与 LexerRule<T> 对象相关的类图。这四个类表示了词法分析器的核心信息,即生成的 DFA 的数据。
图 1 与 LexerRule<T> 相关的类图
LexerRule<T>.CharClass 属性保存了与字符类相关的数据,这是一个长度为 65536 的数组,保存了每一个字符所属的字符类。使用从 0 开始的连续整数表示不一样的字符类,全部包含的字符类的数量可从 LexerRule<T>.CharClassCount 属性获取。关于字符类的详细信息,请参考《C# 词法分析器(四)构造 NFA》的第三节“划分字符类”。
LexerRule<T>.Contexts 属性保存了与词法分析器的上下文相关的数据,这是一个字典,其键为上下文的标签,值为相应的 DFA 头节点索引。LexerRule<T>.ContextCount 属性表示了上下文的数量。
LexerRule<T>.Symbols 属性是定义的终结符的列表,列表的每一项都是一个 SymbolData<T> 结构,包含终结符的标识符、动做和向前看信息。
LexerRule<T>.States 属性是词法分析器的 DFA 状态的列表,列表的每一项都是一个 StateData 结构,包含相应 DFA 状态的转移和对应的终结符索引。这个列表中实际上包含 ContextCount×2 个 DFA,这些 DFA 的首节点索引是从 0 到 ContextCount×2-1,其中每一个上下文对应 2 个 DFA,前一个 DFA 对应于当前上下文中的全部非行首规则,用于从非行首位置进行匹配;后一个 DFA 对应于当前上下文中的全部规则,用于从行首位置进行匹配。索引为 i 的上下文,对应的两个 DFA 就是 i*2 和 i*2+1。关于行首和非行首规则的详细信息,请参考《C# 词法分析器(四)构造 NFA》的第四节“多条正则表达式、限定符和上下文”。
以上就是词法分析器所需的信息,只要获取了这些信息,就能够根据须要,构造本身的词法分析器。详细的实现,请参考《C# 词法分析器(六)构造词法分析器》中提供的算法,甚至能够将数据写入 .cs 文件中(甚至可使用其它语言实现,由于数据自己是不影响实现的),实现词法分析器的生成(虽然如今我还仍未实现这点)。
以上的数据,所有是以比较容易理解的形式存储的,未进行压缩,因此可能会占用比较多的空间。在具体的实现中,能够根据须要改变数据存储格式,或选用一些压缩算法(如使用四数组压缩 DFA 状态)。