[翻译]编译器(10)-编译到 C

原文在此

————翻译分隔线————

编译器(10)-编译到 C

第一部分:介绍
第二部分:编译、转译和解释
第三部分:编译器设计概览
第四部分:语言设计概述
第五部分:Calc 1 语言规格说明书
第六部分:标识符
第七部分:扫描
第八部分:抽象语法树
第九部分:解析

终于到最后一个步骤了!

我们的语言规格说明书如此简单,其实可以跳过 C 直接输出汇编。我有两个不这么做的原因。首先,移植性。在这个指引中,我无须编写任何特定架构的 C 代码。C 已经被移植到各种不同的系统中去了,因此可以让 C 编译器为我们做这个工作。

其次,对于许多程序员来说,汇编比起 C 来说要陌生得多。即使你从未使用 C 编写任何东西,它也比汇编要容易理解得多。

编译

这看起来会有点熟悉。我们将遍历 AST 同时生成 C 代码并进行计算。

编译器生成并输出到一个与原文件相同名字,扩展名为“.c”的文件中。CompileFile 创建输出文件,解析源代码并启动输出处理。

与解析器相同,AST 的第一个元素将是 File 对象。不过 File 对象本身并未包含除了根表达式之外的任何有用的东西,它提供了输出内容的机制。

这一部分并未有任何令人兴奋的内容。只是一系列的模板。我们为程序提供了一个入口,一个退出状态,以及一个必须的 printf 语句和 C 标准输入/输出头文件。

优化

在本系列的开头,我说过不会讨论过多关于优化的内容,事实如此。从另外一个角度来说,我们的语言适合一种优化:预计算。

我们可以检视树中每个元素的对应的代码,但是结果如何呢?它会如此复杂,并且毫无益处。换个角度,为什么不在进行这个步骤的时候就进行计算,让最终的输出简单并且迅速呢?

如果根表达式是一个数,那么就将这个数传递给 printf。如果它是个二值表达式,会有其他一些乐趣。

第一站是通用函数 compNode,用它来判断我们有什么对象:BasicLit 还是 BinaryExpr。

如果是基本语法元素,也就是我们所熟知的整数,只需要将其转换到实际的整数,并返回结果。

二值表达式也是同样简单的。表达式列表中的第一个元素永远都是起点。运算数的顺序对于像除法、减法这样的运算来说非常重要。表达式列表的第一个元素作为起点保存在一个临时变量里(换句话说,这有些类似使用汇编中的 eax 或 rax 寄存器)。

然后基于运算符,针对每个运算数计算出结果并且保存回相同的变量。完成后返回结果。这一过程会递归进行,知道所有内容都完成。

最终环节

在编译器完成了这些工作之后,还有一些需要完成的内容。首先,需要创建一个命令来读取 Calc 1 的源代码,并且调用编译器的库。其次,C 编译器(例如 gcc 或 clang)会针对编译器命令的输出内容进行工作。最后,鉴于 C 编译器的运行方式,你可能还需要对 C 编译器输出的目标文件执行链接器。

所有这些是由一个叫做 calcc 的程序处理的,一个可选的 Calc 编译器。这是我超级聪明的命名技巧的另外一个验证。

这个程序没有什么特别的。它打开一个输入的文件,验证其具有 .calc 的扩展名,然后对其调用 comp.CompileFile。然后使用来自 Go 标准库的 exec.Command 来执行 C 编译器和链接器。

还有若干命令行参数来对 C 编译器进行控制。

结束语

我希望这对于那些想要了解并学习一些关于编译器知识的人有所帮助。这是一个相当庞大的话题,并且几乎无法驾驭。

对于某些感觉这个系列可能过于简单的人来说,我只能抱歉了。我希望你们能有空和我一起,继续向前推进,到达 Calc 2。

我跳过了大量的内容,因此我希望能在接下来的系列中招手解决这些缺陷。将在 Calc 2 中包含的内容有:

  • 符号表
  • 作用域
  • 函数定义
  • 变量定义
  • 对比和分支
  • 变量赋值
  • 类型
  • 内存栈

我将会基于 Calc 1 的代码来实现 Calc 2,因此在这里你所学到的任何东西,我想,都会适用于下一个系列。

如果 Calc 3 最终完成,我真心希望如此,肯定会纠缠于汇编。如果这样的话,可能需要单独包含一些关于汇编本身的文章,或者作为 Calc 3 的独立的导引系列。还有其他一些想法:对象、方法、多次赋值、多文件和库。

感谢阅读!

祝你好运,再见!

Leave a comment

Your email address will not be published. Required fields are marked *