3.4 解析中的数据
当解析器执行时,它需要存储解析过程中的数据。这些数据可能是一个完整的语法树,但大多数情况下不是这样的。即使这种情况出现了,还是需要存储其他的一些数据,以便解析工作可以正常进行。
解析本质上是一种树遍历(见图3-3),当处理某一部分DSL脚本时,对于正在处理的语法树分支,我们可以得到其上下文的一些相关信息。然而,通常我们还会用到这个分支以外的信息。我们再从状态机的例子里选取一段代码看看:
commands
unlockDoor D1UL
end
state idle
actions {unlockDoor}
end
我们在这里看到了一种常见的情况:命令定义在语言的某个地方,然后在其他地方引用。当命令在语句的行为中引用时,我们所在的语法树分支不同于命令定义的分支。如果语法树的表示只存在于调用栈中,那么到这里,命令定义就已经消失了。因此,要把命令对象保存下来以备后用,这样,在行为代码中就可以引用了。
为了做到这一点,我们用到了“符号表”(第12章),它本质上是一个字典,其键是标识符unlockDoor,值是在解析中表示命令的对象。当处理文本unlockDoor D1UL时,创建一个对象持有数据,然后,把它存放在符号表里,键为unlockDoor。存放的对象可能是命令的语义对象,也可能针对局部语法树的中间对象。稍后,当处理actions {un lockDoor}时,我们会通过符号表查找这个对象,以获得状态同行为之间的关系。因此,符号表对于交叉引用至关重要。如果在解析中创建一棵完整的语法树,理论上,可以省略符号表,虽然通常它依然是一个有用的结构,可以把事物关联起来。
在本节结束之前,我们看两个具体的模式。之所以在这里提起它们,是因为内部DSL和外部DSL都会用到。所 以,这里是个合适的地方,即便本章讨论的大多是一些高层次的内容。
当进行解析时,要保存结果。有时,所有结果都可以放到符号表里;有时,许多信息要保存在调用栈里;还有时,要在解析器里有额外的数据结构。在所有这些情况里,最明显要做的一件事是,创建“语义模型”(第11章)对象保存结果。然而,在很多情况下,要到解析的最后时刻才能创建语义模型,所以,还要创建一些中间对象。对于这种中间对象,一个常见的例子是“构造型生成器”(第14章),它是一个对象,包含语义模型所需的全部数据。如果语义模型在创建后就是只读的,这种做法就非常有用了,可以在解析过程中逐步地为它收集数据。构造型生成器拥有同语义模型一样的字段,但这些字段是可读写的,这样,就有地方保存数据了。一旦有了所有数据,就可以创建语义模型对象了。使用构造型生成器会让解析器变得复杂,但相比于改变语义模型的只读属性,我宁愿选择这么做。
事实上,有时候,我们会在处理完所有DSL脚本时,再创建语义模型对象。在这种情况下,解析就会有不同的阶段:首先,读取DSL脚本,创建中间的解析数据,其次,处理中间数据,组装语义模型。在文本处理阶段做多少工作,后面做什么,这取决于语义模型如何组装。
表达式的解析方式取决于我们处理的上下文。查看下面这段文本:
state idle
actions {unlockDoor}
end
state unlockedPanel
actions {lockDoor}
end
当处理actions{lockDoor}时,有一点很重要,它处于unlockedPanel状态的上下文中,而非空闲态。通常,解析器构建以及遍历解析树的方式,就提供了这个上下文,但还有很多情况,很难做到这一点。如果检查解析树无法获得上下文,那么一种好的做法就是,持有上下文,对于这个例子,我们可以把当前状态保存在一个变量里。我将这种变量称为“语境变量”(第13章)。这种语境变量类似于符号表,可以持有语义模型对象,或者一些中间对象。
虽然语境变量用起来很简单,但一般来说,我倾向于尽可能避免使用。语境变量会让解析代码难于理解,正如大量的可变变量会让过程式代码变得复杂。当然,肯定会有无法避免使用语境变量的情况,但我更倾向于将其视为应该避免的坏味道。