C# 词法分析器(七)总结 update 2022.09.13

系列导航

  1. (一)词法分析介绍
  2. (二)输入缓冲和代码定位
  3. (三)正则表达式
  4. (四)构造 NFA
  5. (五)转换 DFA
  6. (六)构造词法分析器
  7. (七)总结

在之前的六篇文章中,比较详细的介绍了与词法分析器相关的算法。本篇则会介绍一下词法分析器相关数据结构,以及如何自定义词法分析器。

一、词法分析器的数据结构

词法分析器的所有数据都存储在 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 实现,而是根据词法分析器数据选择最适合的实现方式。对于简单的字符类,可以选择更精确的查找表,或者直接使用 switchif 替代查表,甚至直接抛弃字符类映射这一步。对于状态转移逻辑,也可以将状态转移表直接展开成 switch 结构。

Cyjb.Compilers 只包含了 C# 的相关实现,也并没有扩展到其它语言的计划。但只要提供类似 Cyjb.Compilers.Runtime 的运行时框架,就可以较为容易的扩展到多语言。

或者还可以考虑使用各类语法声明文件代替目前的 Attribute 声明形式。

posted @ 2014-01-09 12:46  CYJB  阅读(9120)  评论(4编辑  收藏  举报
Fork me on GitHub