太长不看(TL;DR), 你可能在以下情况下需要 Tokenizer:

  1. 需要复用已有框架, 而它恰好提供了 Tokenizer.
  2. 需要回溯上下文, 通常用于报错和语法高亮.

我正在设计和编写一门编程语言 Styio. 在项目之初, 我就有所疑惑: 一个解析器 (Parser) 一般由 Lexer 和 Tokenizer 两部分组成, 其中 Lexer 的存在毋庸置疑, 总要先解析字符串才能从中提取语义信息, 这很好理解; 不过, Tokenizer 的存在价值却并不高, 因为 Lexer 可以直接从字符串生成 AST (抽象语法树, Abstract Syntax Tree), 进行一次额外的 Token 转换反而会带来更大的性能损耗. 所以, 我省略了 Tokenizer, 手写递归下降解析器, 直接从 字符串 生成 AST.

随着 Styio 的不断完善, 我开始需要设计报错信息. 而就在这时候, 我发现自己需要在解析出错的位置做标记, 并且根据其前后的语义信息生成报错信息. 在没有 Tokenizer 的情况下, 我必须重新从字符串再进行一次解析, 才能获得前后文的语义, 但如果存在 Tokenizer, 就可以直接从 Token 开始分析, 省略重复的字符串解析环节.

实际上, Tokenizer 是在字符串的基础上进行的第一层抽象, 也是最基础的抽象. 它的价值并不在于生成 AST, 而在于保存操作结果. 当我们需要回溯分析的时候, Token 的抽象层级就已经足够, 而回到 String 的层级却需要把已经解析过的字符串再解析一遍, 这是额外的性能损耗.

在经过衡量之后, 我依然选择了 放弃 Tokenizer. 因为 Styio 的语法极为复杂和自由, Lexer 完全有能力在当前位置抛出有价值的报错信息. 不过, Tokenizer 确实对于语法高亮等下游任务极为有用. 因此, 我选择采取一种反方向的设计: Styio 的 Parser 将会在字符串生成 AST 之后, 保存当前的 Token, 而不是消耗当前的 Token, 作为历史信息以供后续回溯.


unka_malloc
7 声望3 粉丝