C# 词法分析器(七)总结 update 2022.09.13
系列导航
在之前的六篇文章中,比较详细的介绍了与词法分析器相关的算法。本篇则会介绍一下词法分析器相关数据结构,以及如何自定义词法分析器。
一、词法分析器的数据结构
词法分析器的所有数据都存储在 LexerData<T> 类中,主要包括:
- 上下文数据
IReadOnlyDictionary<string, ContextData<T>> Contexts
- 终结符列表
TerminalData<T>[] Terminals
- 字符类映射
CharClassMap CharClasses
- DFA 的四数组表示:
DfaStateData[] States
int[] Next
int[] Check
- 向前看符号的类型
TrailingType TrailingType
- 词法分析中是否包含与行首匹配对应的头节点
bool ContainsBeginningOfLine
- 是否用到了 Reject 动作
bool Rejectable
- 词法分析控制器的类型
Type ControllerType
1.1 字符类映射
为了兼顾字符到字符类的转换效率和内存占用,没有使用 Dicitonary<char, int>
或者 int[65536]
,而是选择了 int[128]
优化常用 ASCII 范围的转换效率,剩余字符使用字符范围二分查找和 Dictionary<UnicodeCategory, int>
实现映射。
如果字符在 ASCII 范围内,可以直接查表返回。
其它字符现在字符范围内二分查找,如果找到了就返回字符范围对应的字符类。
否则转换为相应的 Unicode 类别,查字典得到结果。
否则表示不属于可以识别的字符。
具体实现可以参见 CharClassMap 类。
1.2 DFA 的四数组表示
一般来说,DFA 可以使用状态转移表来表示,一般是一个二维表格。以上一节 4.3 转移的字符串 中定义的词法分析器为例,下图左侧是其 DFA,右边是对应的状态转移表。
图 1 示例 DFA
状态转移表的每行表示 DFA 的一个状态,每列表示 DFA 的一个转移字符(或字符类),上面的 DFA 包含 7 个状态,以及 6 个转移字符类,总计 19 条转移。
使用状态转移表来表示 DFA,运行效率高,而且易于阅读和调试。但是状态转移表中存在大量的无效转移(使用空白表示无效转移),导致状态转移表需要占用大量空间。在空间较为紧缺的情况下,就需要对状态转移表进行压缩,以提高空间利用率。
一种比较实用的办法就是使用三数组和四数组压缩,它们都是尽量重复利用状态转移表中的空转移。
如下图所示,简单说来就是将不同状态相互错开,使得有效转移之间不会相互覆盖(a → b),然后就可以将多个状态的转移压缩到一个数组中,这个数组被称为 next 数组(b → c)。错开后的索引记录在 base 数组中,为了明确 next 数组中记录的值属于哪个状态,就要把所属状态存储到另一个 check 数组中。
图 2 三数组压缩
next 和 check 数组的长度是相同的,与压缩后的转移个数有关;base 数组的长度则总是与状态数一致。
要访问特定状态的转移的时候,首先利用 base[state] + charClass
,计算得到与当前状态 state
和字符类 charClass
对应的转移索引 idx。然后检查 check[idx]
是否等于 state
,如果相同,那么 next[idx]
就是下一状态,否则下一状态为空。
在理想情况下,next 和 check 数组中的空间应当都是被有效利用的,长度略大于转移个数。但是在实际情况中,可能难以实现高效计算。不过下述简单策略就能有较好的效果:总是按顺序计算每个状态,将 base[state]
指定为最小的、能够使得状态 state 的所有转移位置都尚未被占用的值。
四数组压缩与之类似,只是会多一个额外的 default 数组,长度与 base 一致。其思想是 DFA 中存在一些较为相似的状态,假设状态 B 与状态 A 的转移较为相似,就可以只将 B 的独特转移保存在三数组中,再设置 default[B] = A
,在未找到合适转移时直接使用状态 A 的转移。
LexerData<T> 中使用的就是四数组表示,相关构建算法可以参见这里。
二、自定义词法分析器
正常构造词法分析规则后,使用 Lexer<T, TController>.GetData()
方法就可以拿到词法分析器的所有相关数据,然后就可以根据需要进行自定义。
例如在对性能较为敏感的场景,不套用现有的 Tokenlizer
实现,而是根据词法分析器数据选择最适合的实现方式。对于简单的字符类,可以选择更精确的查找表,或者直接使用 switch
和 if
替代查表,甚至直接抛弃字符类映射这一步。对于状态转移逻辑,也可以将状态转移表直接展开成 switch
结构。
Cyjb.Compilers 只包含了 C# 的相关实现,也并没有扩展到其它语言的计划。但只要提供类似 Cyjb.Compilers.Runtime 的运行时框架,就可以较为容易的扩展到多语言。
或者还可以考虑使用各类语法声明文件代替目前的 Attribute 声明形式。
作者:CYJB
出处:http://www.cnblogs.com/cyjb/
GitHub:https://github.com/CYJB/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。