本章导引
本书的这一部分介绍 Scala 框架各阶段实验的主要任务以及框架中一些重要的实现方法。
分阶段设计
作为系统软件的重要一员,编译器是连接用户与机器的桥梁。编译器的核心功能是: 将某种对人类(相对)友好的高级语言语义保持地(但很多编译器并不能完全做到)转换为机器可执行的低级语言。 随着计算机学科的飞速发展,编译器的功能已远不止于此,代码优化 (optimization)、静态分析 (static analysis/lint),乃至属性验证 (verification) 和程序综合 (synthesis),已逐渐成为现代工业级编译器的标配。
编译器是一个复杂的软件,复杂到即便用上很多现代软件工程的技术手段,也远不是任何普通团队和个人能轻易掌控的。 但是,编译器也不神秘,它不就是个把高级语言翻译成低级语言的程序吗?这里最大的挑战是:高级语言往往很高级,低级语言往往很低级。 如何打破这跨越时代的变迁?分层!
分层这个概念大家并不陌生:计算机的软硬件分层、计算机网络的 OSI 七层模型与 TCP/IP 五层模型、商业网站的开后端分离、甚至深度学习框架也开始搞开后端分离。 既然高级语言和低级语言差异很大,不如设计一系列中间语言,逐个击破,多次翻译,那么就在有限的步数内解决问题。 Decaf 的编译器也是如此,在 Decaf 源码和 Mips 汇编之间,我们设计了抽象语法树、带类型标注的抽象语法树、TAC 这三种中间语言。
在语法分析阶段,Decaf 源码被转换为抽象语法树(PA1)
在语义分析阶段,抽象语法树被转换为带类型标注的抽象语法树(若程序无类型错误)(PA2)
在中间代码生成阶段,带类型标注的抽象语法树被转换为 TAC(PA3)
在汇编码生成阶段,TAC 被最终转换为 Mips 汇编(PA5)
此外,在生成汇编码前,我们还可以对 TAC 进行一系列优化,以生成执行效率更高的 TAC(PA4)。
在 Scala 框架中,上述每个阶段都被定义为一个 Phase
:
abstract class Phase[In, Out](...) extends Task[In, Out] with ErrorIssuer
每个阶段都是一个可能失败的函数 In => Option[Out]
,其中 In
为该阶段作为输入的某种语言表示,而 Out
为该阶段作为输出的某种语言表示。 例如,文法分析阶段就是从某个输入流读取 Decaf 源码,输出语法树:
class Parser extends Phase[InputStream, Tree](...)
而一次完整的编译过程,无非就是把这些阶段像流水线一样给拼接 (pipe) 起来。由于每个阶段都是一个有可能失败的函数,这种拼接就对应于函数的某种组合:
trait Task[-T, +U] extends Function[T, Option[U]] {
def |>[R](g: Task[U, R]): Task[T, R] = x => this (x) flatMap g
}
具体来说,f |> g
就是,先做 f
,如果成功,把输出的结果作为输入传给 g
,返回 g
的结果(成功或者失败)。 如果 f
已经失败了(执行 f
的过程中产生错误),那么直接终止整个流程,返回失败。 用 Haskell 的话讲,这 (|>
) 不就是个 Kleisli composition 吗?
借助这个 |>
组合子,我们能方便地定义出各阶段编译器要执行的任务:
val parse = new Parser
val typeCheck: Task[InputStream, TypedTree.Tree] = parse |> new Namer |> new Typer
val tac: Task[InputStream, TacProg] = typeCheck |> new TacGen
val jvm: Task[InputStream, List[JVMClass]] = typeCheck |> new JVMGen
val optimize: Task[InputStream, TacProg] = tac |> new Optimizer
val mips: Task[InputStream, String] = optimize |> new Asm
代码结构
本框架的源码部分的包组织如下:
decaf
.driver 主控(命令行选项解析、任务调度)
.error 错误定义及处理
.frontend 前端
.tree 语法树定义
.annot 语法树标注(类型、符号、作用域)定义
.parsing 语法分析
.typecheck 语义分析(符号表构建、类型检查)
.tac 中间代码 TAC 生成
.backend 后端
.dataflow 数据流分析
.opt TAC 优化
.reg 寄存器分配
.asm 汇编码生成
.jvm JVM 字节码生成
.printing 格式化打印
.util 辅助类
.lowlevel 底层工具链(Java 编写,未来会分离出来)
.instr (伪)指令定义
.label 标签定义
.tac TAC 工具链
.log 简单 Logger(供调试使用)
此外,src/main/antlr4
包含了 Antlr 需要使用的词法和文法描述文件。
项目构建
目前本框架基于 Scala 的官方工具 sbt
构建。虽然它有时会很慢,但对于这个项目来说还算可以接受。 使用 sbt
的一个主要优点在于,你可以直接 sbt console
打开 Scala REPL,方便地对项目中编写好的类和方法进行调试和测试。 工程引入了两个插件:
Antlr 插件能在编译时进行代码生成
assembly 插件便于创建独立的
.jar
包(命令sbt assembly
)
此外,你可以使用 sbt doc
生成 ScalaDoc 风格的 API 文档,以便查阅和搜索。
与 Java 混合编程
本框架会涉及到与 Java 混合编程的部分有:
文法分析器:调用 Antlr 生成的 Java 方法
TAC 生成:调用
decaf.lowlevel.tac
中提供的ProgramWriter
辅助完成 TAC 的生成和模拟执行,未来将支持生成 TAC 字节码JVM 字节码生成:调用
org.objectweb.asm
提供的方法辅助完成 JVM 字节码生成目标平台定义:
decaf.lowlevel
中提供了 Mips 等目标平台的基本定义,如(会用到的)指令集和寄存器等
由于 Scala 在设计时就考虑了与 JVM 平台其他语言(以 Java 为代表)的兼容性,使用 Scala 和 Java 混合编程并不困难。 在混合编程时,如何在 Java 与 Scala 的数据结构之间转换是一个高频需求。 为此,Scala 在 scala.jdk.CollectionConverters
包中提供了一组辅助函数,但是你仍需通过显式调用 .asJava
/.asScala
的方式完成转换。 得益于 Scala 的隐式转换特性,我们在 decaf.util.Conversions
中封装了一组隐式转换方法。 你只要在代码中导入它们,就能实现 java.util.List
与 scala.List
,以及 java.util.Optional
与 scala.Option
之间的无缝转换。
Last updated
Was this helpful?