



要将一种语言编译成另一种语言,聚焦两种语言之间的差异是非常有帮助的,因为编译器需要弥合这些差异。 L Var 语言和x86汇编语言之间有什么区别?下面是一些重要的不同点:
●x86汇编语言算术指令通常有两个参数,并在执行后就地更新第二个参数。相比之下, L Var 语言算术运算接受两个参数并产生一个新值。一条x86汇编指令最多只能有一个内存访问参数。此外,一些x86汇编指令对它们的参数有特殊的限制。
● L Var 语言操作符的参数可以是一个深度嵌套的表达式,但x86汇编指令将其参数限制为整数常量、寄存器和内存位置。
● L Var 语言中的程序可以有任意数量的变量,而x86汇编具有16个寄存器和过程调用的栈。
我们将通过分解问题为几个步骤来处理这些差异,从而应对将
L
Var
语言编译到x86汇编的挑战。这些步骤中的每一步都称为编译器的一
遍
。这个术语表示每一步都走过或遍历程序的AST。此外,我们遵循微遍
的方法,这意味着我们力求每进行一遍完成一个明确的目标,而不是同时完成两三个目标。我们首先概述如何实现各微遍,并为每个遍命名。然后,我们确定各遍的顺序和输入/输出语言。第一遍使用
L
Var
作为其输入语言,最后一遍使用x86
Int
作为其输出语言。在这两遍之间,我们可以选择便于表达每遍输出的语言,无论是
L
Var
、x86
Int
还是我们自己设计的新的
中间语言
。最后,为了实现每一个微编译遍,我们会为该遍的输入语言语法中的每一个非终结符编写一个递归函数。
我们的 L Var 编译器由以下几个编译遍组成:
●remove_complex_operands(移除复杂操作数)确保原语操作或函数调用的每个子表达式都是变量或整数,即原子表达式。我们把非原子表达式称为 复杂表达式 。这一遍引入临时变量来保存复杂子表达式的中间结果。
●select_instructions(选择指令)处理 L Var 语言操作和x86汇编指令之间的差异。这一遍将 L Var 语言中的每个操作转换为完成相同任务的短指令序列。
●assign_homes(分配变量存储)用寄存器或栈位置替换变量。
下一个问题是,我们应该以什么顺序应用这些编译遍进行处理?这个问题可能具有挑战性,因为很难事先知晓哪种安排更好(即更容易实现,生成更高效的代码,等等),因此,安排次序通常涉及使用试错法。然而,我们可以提前计划,并做出明智的选择。
select_instructions和assign_homes这两遍是交织在一起的。在第8章中,我们会了解到,在x86汇编中寄存器用于向函数传递参数,并且最好将参数赋值给相应的寄存器。这表明,在执行寄存器分配之前,最好从select_instructions这遍就开始考虑此问题,生成用于传递参数的指令。另一方面,如果先选择指令,我们可能会在assign_homes遍陷入死胡同中。因为x86汇编指令只有一个参数能访问内存,但是assign_homes遍中可能会强制为两个参数都分配内存。一个复杂的方法是重复这两个步骤,直到找到解决方案。然而,为了降低实现的复杂性,我们建议首先进行select_instructions遍,然后是assign_homes遍,之后是第三个名为patch_instructions的遍,第三个遍使用保留寄存器来修复突出的问题。
图2.10给出了编译器各编译遍的执行顺序,并标识了每遍的输入和输出语言。select_instructions遍的输出是x86 Var 语言,它使用无限作用域变量来扩展x86 Int 语言,并删除指令参数限制。最后一个是prelude_and_conclusion遍,将程序指令放入一个带有起始和收尾指令的main函数中。本章的剩余部分中提供了图2.10所示的编译器进行的各遍的实现指南。
图2.10 L Var 语言编译各遍的示意图