一些事儿 - achun/tom-toml GitHub Wiki

为什么要再写一个TOML解析器

  • 学习写解析器
  • 支持注释

学习写解析器

一直认为编写解析器是非常有挑战性的任务. TOML 本身已经很简洁. 为 TOML 写个解析器很有吸引力. 我们知道已经有了 YACC 这样工具可以完成此类工作. 事实上 Go 提供了这样的工具, TOML 上也有关于 EBNF 的讨论, 已经出现出几个版本. 但是要让这些 EBNF 定义转换成特定语言的代码, 还有很多辅助工作. 作为学习目的, 我采取先手工写一个解析器, 可以对完整使用 EBNF 有更深刻的理解.

鉴于 TOML 的简洁. 手工写出所有的 First 集和 Follow 集是可行的. parser.go 中 stateEmpty/tokensEmpty 就是 First 集, 按照编译原理所阐述的, 解析完整结束也会回到 First 集. 解析开始的时候至少要匹配到 First 集合中的一项(TOML 没有二义性, 只匹配一个). 解析结束的时候会回到 First 集合, 由于我写的 First 集合中没有 EOF 匹配, 所以当匹配不了 First 集合时, 解析结束, 相反如果在 First 集合中写下 stateEof/tokensEof 那最终会以匹配 EOF 而结束. 其他的 Follow 集合也是必须要有匹配, 如果没有被匹配, 那表示输入无效, 实现中我在每个 stateXX 中增加了一个没有匹配到要执行的动作, 用来给出一点提示信息.

扫描器

解析器是有明确的阶段, 其中词法分析(也可以称为扫描器)是第一阶段. 对于手工写的解析器, 这些阶段的代码可以混合在一起. tom-toml 的扫描器 Scanner 是一个纯粹的 UTF-8 字符扫描器, 每次只扫描一个 UTF-8 字符, 别致的地方在于 Scanner 消除了 token 匹配中常见的 peek 操作. First 集和 Follow 集具体的匹配代码写法和这种 Scanner 是配合的, 所以在 itsString 这样的 token 匹配代码中可以看到 flag 这个状态标志, peek 被消除了. 当然这种方法只是一种尝试, 我并不确定是否可以普遍适用. 采用这种写法有个原因维护 peek 总让我晕头转向.

支持注释

TOML 的实现有很多, 在 tom-toml 之前, 很多实现都是不支持注释操作的, 我认为注释是必要被支持的. go-toml fork 自 pelletier/go-toml 并增加了注释支持, 好像 pelletier 不理解支持注释是必要. 鉴于改造的比较大, 不如重新写一个解析器.

Value 和 Item

tom-toml 在实现中, 把 TOML 定义中的段(Table/ArrayOfTables)和值(String, Integer ...)分开进行定义. 事实上 Table 的存储也被 Value 负责, 在 tom-toml 中 Table 实际上就是个空的 Value. 因此会有这样的判断代码

func (p *Value) IsValid() bool {
	return p.kind != InvalidKind && (p.v != nil || p.kind == Table)
}

保留这个空的 Table 对 Toml 对象格式化输出TOML文本是有意义的.

Value 的方法 Int/String/Float/Boolean/Datetime 是仿照 reflect.Value 的方法设计的. 也就是说使用者要自己确定 Value 的 Kind 并调用相应的方法获取数据的值, 如果错误的调用(String方法特殊, 其他类型可以转换到 string), 方法不会产生错误, 会返回一个缺省值.

Item 扩展自 Value, 目前是为了支持 ArrayOfTables 的, 可以看出 Value 主要负责存储值的维护, Item 维护了复杂的类型定义.

贡献

如果您有任何问题, 建议请 issues 反馈.