抽象语法树
抽象语法树 (Abstract Syntax Tree, AST) 是贯穿 Decaf 编译器整个前端的重要数据结构。 “抽象语法”是相对于“具体语法”而言的。在具体语法树 (Concrete Syntax Tree, CST) 上,所有语法符号都会被记录。 而在 AST 上,那些我们在编译器后面的阶段不再需要关心的符号(如逗号等)则被忽略。 简而言之,AST 是一种在忽略冗余语法细节的前提下,对源代码结构的忠实表达。
在 Scala 框架中,我们分别定义了不带类型标注的 AST (SyntaxTree
) 和带类型标注的 AST (TypedTree
)。 它们有诸多类似的 AST 结点。考虑二元运算表达式 Binary
。无论是否有类型,Binary
都至少包含了三个元素:二元操作符及两个操作数。 但是,在 SyntaxTree
中,这两个操作数是不带类型的,因此它们的类型应该是 SyntaxTree.Expr
。 而在 TypedTree
中,这两个操作数是带类型的,因此它们的类型应该是 TypedTree.Expr
。此外,我们还要能够得到 TypedTree.Expr
的类型。 为此,我们期望能有一种方法,在不重复写 Binary
两次的情况下,也能定义出这两种实际上不同的语法结点。
AST 模板
在 TreeTmpl.scala
中,我们给出了一个一般化的 AST 模板。 该模板包含了 SyntaxTree
和 TypedTree
共有的语法结点。同时,我们允许每个语法结点有一个额外的域来存储如类型这种“标注” (annotation)。 标注定义在 decaf.frontend.annot
包中。例如:
我们将该语法树结点的三个元素定义在第一个参数组里。其中,Expr
是指这个 trait 里面的那个 Expr
类型。 而标注则定义在第二个参数组里,并作为隐式参数 (implicit parameter)。 这个标注的类型是抽象的,我们先约束其上界为 Annot
(所有标注类型都得是它的子类型),然后等实例化的时候再指定即可。 这样做的好处是当这个标注为空时,我们可以定义一个隐式的空标注,那么在构造该结点时就可以直接忽略第二个参数组了。
模板实例化
在我们实例化上述模板时,例如实例化成 SyntaxTree
:
由于 SyntaxTree
无需标注,我们先定义一个 NoAnnot
当作空标注。 并在 SyntaxTree
中重写 ExprAnnot
类型成员为 NoAnnot
这个单例对象的类型 NoAnnot.type
。 这样,Scala 编译器在进行 trait mixin 时,上述代码会被展开为(示意):
这样我们就得到所期望的 SyntaxTree.Binary
了。由于 NoAnnot
是一个隐式值,且类型为 NoAnnot.type
。那么在构造语法树结点时,
会被展开为
注意隐式参数 NoAnnot
被编译器自动查找到并填充。
完全类似地,TypedTree
的定义也很简单:
只需知道表达式的标注是 Type
。但是,目前我们只能通过 .annot
来读取其标注的类型。 这似乎不是特别友好——光从 annot
的名字看不出来它到时是何种标注。而 typ
似乎是一个对于类型标注来说更合理的名字。 为了实现这个特性,我们使用 Scala 的隐式类 (implicit class):
即:让任何标注了 Type
的对象 self
都具有一个 typ
方法,它的返回 self.annot
,而此时 self.annot: Type
。 于是,我们就可以直接这样取得一个语法树结点的类型:
AST 就是一棵树
从名称来看,AST 就是一棵树。在本框架的实现中,我们也贯彻了这一想法,即让每一个 AST 结点都具有树状的结构。 具体来说,对于任何一个 AST 结点,我们需要知道它有几棵子树,以及这些子树分别是什么。 特别地,如果一个 AST 结点不包含任何子树,那么它就是叶子结点。
Scala 的 case class 天生支持我们期望的功能——只需将所有 AST 结点定义成 case class,并且把子树依次作为该类(主)构造函数的第一个参数组。 回顾我们对于 Binary
的定义:
其构造函数的第一个参数组包含了 op
, lhs
和 rhs
。它们构成了 Binary
的三棵子树:二元操作符、左操作数和右操作数。
在本框架中,这样设计的最大好处在于——大幅简化格式化打印 AST 的代码量!打印策略的实现(见 PrettyTree.scala
)非常简单:
打印名称 (
productPrefix
)增加缩进
遍历所有子树 (
productIterator
),依次递归打印它们减少缩进
注意到 productPrefix
和 productIterator
都已经是 case class 的成员。 Scala 编译器在实现 case class 时,会让它们继承 scala.Product
并自动实现上述两个接口。
评注:Scala Implicit
Implicit 是 Scala 的一大特色,隐式转换、隐式参数极大地提高了编码效率,也使得 Scala 成为一门极易于构建内置式领域特定语言 (embedded domain-specific language) 的通用程序语言。 但是,由于 implicit 变幻莫测,对新手不太友好,滥用 implicit 会导致项目变得难以维护,甚至行为不可控。这导致很多用户并不喜欢这个特性。 这有点像 Haskell 的 monad,新手觉得晦涩难懂,专家用得出神入化。 Scala 的下一代版本将会对 implicit 这一特性进行大改版,以符合更多工业界人的口味。 在本框架中,我们仅在必需的场合使用了部分常见 implicit 特性,以降低编码冗余。
Last updated