目录
综述
我们把编译器(compiler)看作一个黑盒子,它能够把源程序映射为在语义上等价的目标程序。如果把这个盒子稍微打开一点,我们就会看到这个映射过程由两个部分组成:分析部分和综合部分。
分析(analysis)部分把源程序分解成为多个组成要素,并在这些要素之上加上语法结构。然后,它使用这个结构来创建该源程序的一个中间表示。如果分析部分检查出源程序没有按照正确的语法构成,或者语义上不一致,它就必须提供有用的信息,使得用户可以按此进行改正。分析部分还会收集有关源程序的信息,并把信息存放在一个称为符号表(symnbol table)的数据结构中。符号表将和中间表示形式一起传送给综合部分。
综合(synthesis)部分根据中间表示和符号表中的信息来构造用户期待的目标程序。分析部分
经常被称为编译器的前端(front end),而综合部分称为后端(back end)。
如果我们更加详细地研究编译过程,会发现它顺序执行了一组步骤(phase)。每个步骤把源程序的一种表示方式转换成另一种表示方式。一个典型的把编译程序分解成为多个步骤的方式如图1-1 所示。在实践中,多个步骤可能被组合在一起,而这些组合在一起的步骤之问的中间表示不需要被明确地构造出来。存放整个源程序的信息的符号表可由编译器的各个步骤使用。

有些编译器在前端和后端之间有一个与机器无关的优化步骤。这个优化步骤的目的是在中间表示之上进行转换,以便后端程序能够生成更好的目标程序。如果基于末经过此优化步骤的中间表示来生成代码,则代码的质量会受到影响。因为优化是可选的,所以图1-1中所示的两个优化步骤之一可以被省略。
结构
1 词法分析
编译器的第一个步骤称为词法分析 (lexicalanalysis) 或扫描(scanning)。词法分析器读入组成源程序的字符流,并且将它们组织成为有意义的词素(lexeme) 的序列。对于每个词素,词法分析器产生如下形式的词法单元(token)作为输出:
<token-name,attribute-value>
这个词法单元被传送给下一个步骤,即语法分析。在这个词法单元中,第一个分量 token-name 是一个由语法分析步骤使用的抽象符号,而第二个分量attribute-value 指向符号表中关于这个词法单元的条目。符号表条目的信息会被语义分析和代码生成步骤使用。
比如,假设一个源程序包含如下的赋值语句
position = initial + rate * 60 (1.1)
这个赋值语句中的字符可以组合成如下词素,并映射成为如下词法单元。这些词法单元将被传递给语法分析阶段。
- position 是一个词素,被映射成词法单元 < id, 1 >,其中id 是表示标识符(identifier)的抽象符号,而1指向符号表中 position 对应的条目。一个标识符对应的符号表条目存放该标识符有关的信息,比如它的名字和类型。
- 赋值符号 = 是一个词素,被映射成词法单元 < = >。因为这个词法单元不需要属性值,所以我们省略了第二个分量。也可以使用 assign 这样的抽象符号作为词法单元的名字,但是为了标记上的方便,我们选择使用词素本身作为抽象符号的名字。
- initial 是一个词素,被映射成词法单元 < id, 2 >,其中2指向 initial 对应的符号表条目。
- +是一个词素,被映射成词法单元 < + > 。
- rate 是一个词素,被映射成词法单元 < id, 3 >,其中3指向 rate 对应的符号表条目。
- * 是一个词素,被映射成词法单元 < * > 。
- 60 是一个词素,被映射成词法单元 < 60 > 。
注意:从技术上讲,我们应该为语法单元 60 建立一个形如 < number, 4 > 的词法单元,其中4指向符号表中对应于整数60的条目。但是我们要到之后才讨论数字的词法单元。未来将讨论建立词法分析器的技术。
分隔词素的空格会被词法分析器忽略掉。
图1-2给出经过词法分析之后,赋值语向1.1被表示成如下的词法单元序列:
< id, 1> < = > < id, 2> < + > < id, 3> < * > < 60 >
在这个表示中,词法单元名 = 、+ 和 * 分别是表示赋值、加法运算符、乘法运算符的抽象符号。

2 语法分析
编译器的第2个步骤称为语法分析(syntax analysio)或解析( persing)。语法分析器使用由词
法分析器生成的各个词法单元的第一个分量来创建树形的中间表示。该中向裘示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树(syntax tree),树中的每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。在图1-2中,词法单元流(1.2)对应的语法树被显示为语法分析器的揄出。
这棵树显示了赋值语句
position = initial + rate * 60
中各个运算的执行顺序。这棵树有一个标号为 * 的内部结点,< id, 3 > 是它的左子结点,整数60是它的右子节点。结点 < id, 3 > 表示标识符 rate。标号为 * 的结点指明了我们必须首先把 rate 的值与60相乘。标号为 + 的结点表明我们必须把相乘的结果和 initial 的值相加,这棵树的根结点的标号为 = ,它我明我们必须要把相加的结果存储到标识符position对应的位置上去。这个运算顺序和通常的算术规则相同,即乘法的优先级高于加法。因此,乘法应该在加法之前计算。
编译器的后续步骤使用这个语法结构来帮助分析源程序,并生成目标程序。在未来,我们将使用上下文无关文法来描述程序设计语言的语法结构。并讨论为某些类型的语法自动构造高效语法分析器的算法。在之后,我们将看到语法制导的定义将有助于描述对程序设计语言结构的翻译。
3 语义分析
话义分析器(semantic analyzer)使用语法树和符号表中的信息来检查源程序是否和语言定义的语义一致。它同时也收集类型信息,并把这些信息存放在语法树或符号表中,以便在随后的中间代码生成过程中使用。
语义分析的一个重要部分是类型检查(type checking)。编译器检查每个运算符是否具有匹配的运算分量。比如,很多程序设计语言的定义中要求一个数组的下标必须是整数。如果用一个浮点数作为数组下标,编译器就必须报告错误。
程序设计语言可能允许某些类型转换,这被称为自动类型转换 (coercion)。比如,一个二元算术运算符可以应用于一对整数或者一对浮点数。如果这个运算符应用于一个浮点数和一个整数,那么编译器可以把该整数转换(或者说自动类型转换)成为一个浮点数。
图1-2 中显示了一个这样的自动类型转换。假设 position、initial 和 rate 己被声明为浮点数类型,而词素60本身形成一个整数。图1-2中的语义分析器的类型检查程序发现运算符 * 被用于一个浮点数 rate 和一个整数 60。在这种情况下,这个整数可以被转换成为一个浮点数。请注意,在图1-2中,语义分析器输出中有一个关于运算符 inttofloat 的额外结点。inttofloat 明确地把它的整数参数转换为一个浮点数。类型检查和语义分析将在未来讨论。
4 中间代码生成
在把一个源程序翻译成目标代码的过程中,一个编译器可能构造出一个或多个中间表示。这些中间表示可以有名种形式。语法树是一种中间表示形式,它们通常在语法分析和语义分析中使用。
在源程序的语法分析和语义分析完成之后,很多编译器生成一个明确的低级的或类机器语言的中间表示。我们可以把这个表示看作是某个抽象机器的程序。该中间表示应该具有两个重要的性质:它应该易于生成,且能够被轻松地翻译为目标机器上的语言。
在未来,我们将考虑一种称为三地址代码(three-address code) 的中间表示形式。这种中间
表示由一组类似于汇编语言的指令组成,每个指令具有三个运算分量。每个运算分量都像一个奇存器。图1-2中的中间代码生成器的输出是如下的三地址代码序列:
(1.3)
关于三地址指令,有几点是值得专门指出的。首先,每个三地址赋值指令的右部最多只有一个运算符。因此这些指令确定了运算完成的顺序。在源程序1.1中,乘法应该在加法之前完成。第二,编译器应该生成一个临时名字以存放一个三地址指令计算得到的值。第三,有些三地址指令的运算分量的少于三个(比如上面的序列1.3 中的第一个和最后一个指令)。
在未来,我们将讨论在不同编译器中用到的主要中间表示形式。介绍语法制导翻译技术。这些技术被用于处理典型程序设计语言构造进行类型检查和中间代码生成。这些程序设计语盲构造包括:表达式、控制流构造和过程调用。
5 代码优化
机器无关的代码优化步骤试图改进中间代码,以便生成更好的目标代码。“更好”通常意味着更快,但是也可能会有其他目标,如更短的或能耗更低的目标代码。比如,一个简单直接的算法会生成中间代码(1.3)。它为由语义分析器得到的树形中间表示中的每个运算符都使用一个指令。
使用一个简单的中间代的生成算法,然后再进行代码优化步骤是生成优质目标代码的企理方法。优化器可以得出结论,把60从整数转换为浮点数的运算可以在编译时刻一劳永逸地完成。因此,用浮点数60.0 来替代整数 60 就可以消除相应的 inttorloat 运算。而且,t3仅被使用一次,用来把它的值传递给id1。因此,优化器可以把序列(1.3)转换为更短的指令序列
(1.4)
不同的编译器所做的代码优化工作量相差很大。那些优化工作做得最多的编译器,即所谓的“优化编译器”,会在优化阶段花相当多的时间。有些简单的优化方法可以极大的提高目标程序运行效率而不会过多降低编译的速度。未来,将详细讨论机器无关和机器相关的优化。
6 代码生成
代码生成器以源程序的中间表示形式作为输人,并把它映射到目标语言。如果目标语言是机器代码,那么就必须为程序使用的每个变量选择寄存器或内存位置。然后,中间指令被翻译成为能够完成相同任务的机器指令序列。代码生成的一个至关重要的方面是合理分配奇存器以存放变量的值。
比如,使用寄存器 R1 和 R2,(1.4)中的中间代码可以被翻译成为如下的机器代码:
LDF R2, id3
MULF R2, R2, #60.0
LDF R1, id2
ADDF R1, R1, R2
STF id1, R1 (1.5)
每个指令的第一个运算分量指定了一个目标地址。各个指令中的F告诉我们它处理的F是浮点数。代码(1.5)把地址 id3 中的内容加载到寄存器 R2 中,然后将其与浮点常数 60.0相乘。井号 # 表示60.0应该作为一个立即数处理。第三个指令把 id2 移动到奇存器 R1 中,而第四个指令把前面计算得到并存放在 R2 中的值加到R1 上。最后,在寄存器 R1 中的值被存放到 id1 的地址中去。这样,这些代码正确地实现了赋值语句(1.1)。未来将讨论代码生成。
上面对代码生成的讨论忽路了对源程序中的标识符进行存储分配的重要问题。我们将在未来中看到,运行时刻的存储组织方法依赖于被编译的语言。编译器在中间代码生成或代码生成阶段做出有关存储分配的决定。
7 符号表管理
编译器的重要功能之一是记录源程序中使用的变量的名字,并收集和每个名字的各种属性有关的信息。这些属性可以提供一个名字的存储分配、它的类型、作用域(即在程序的哪些地方可以使用这个名字的值)等信息。对于过程名字,这些信息还包括:它的参数数量和类型、每个参数的传递方法(比如传值或传引用)以及返回类型。
符号表数据结构为每个交量名字创建了一个记录条目。记录的字段就是名字的各个属性。这个数据结构应该允许编译器迅速在查找到每个名字的记录,并向记录中快速存放和获取记录中的数据。符号表在后续文章中讨论。
8 将多个步骤组合为趟
前面关于步骤的讨论讲的是一个编译器的逻辑组织方式。在一个特定的实现中,多个步骤的活动可以被组合成一趟(pass),每趟读入一个输入文件并产生一个输出文件。比如前端步骤中的词法分析、语法分析、语义分析以及中间代码生成,可以被组合在一起成为一趟。代码优化可以作为一个可选的趟,然后可以有一个为特定目标机生成代码的后端趟。
有些编译器集合是围绕一组精心设计的中间表示形式而创建的,这些中间表示形式使得我们可以把特定语言的前端和特定目标机的后端相结合。使用这些集合,我们可以把不同的前端和某个目标机的后端结合起来,为不同的源程序建立该目标机上的编泽器。类似地,我们可以把一个前端和不同的目标机后端结合,建立针对不同目标机的编译器。
9 编译器构造工具
和任何软件开发者一样,写编译器的人可以充分利用现代的软件开发环境。这些环境中包含了诸如语言编译器、调试器、版本管理、程序描述器、测试管理等工具。除了这些通用的软件开发工具,人们还创建了一些更加专业的工具来实现编译器的不同阶段。
这些工具使用专用的语言来描述和实现特定的组件,其中的很多工具使用了相当复杂的法。其中最成功的工具都能够隐藏生成算法的细节,并且它们生成的组件易于和编译器的其他部分相集成。一些常用的编译器构造工具包括:
- 语法分析器的生成器:可以根据一个程序设计语言的语法描述自动生成语法分析器。
- 扫描器的生成器:可以根据一个语言的语法单元的正则表达式描达生成词法分析器。
- 语法制导的翻译引擎:可以生成一组用于遍历分析树并生成中间代码的例程。
- 代码生成器的生成器:依据一组关于如何把中间语言的每个运算翻译成为目标机上的机器语言的规则,生成一个代码生成器。
- 数据流分析引擎:可以帮助收集数据流信息,即程序中的值如何从程序的一个部分传递到另一部分。数据流分析是代码优化的一个重要部分。
- 编译器构造工具集:提供了可用于构造编译器的不同阶段的例程的完整集合。