Lecture Notes: Compiler Construction
COMP3173 Compiler Construction
Dr. Goliath Zhiyuan LI
Lecture 1
编译器核心概念 (Core Concepts of a Compiler)
1. 编译器的定义 (Definition of a Compiler)
一个
compiler
(编译器) 是一个程序,它读取用一种语言(source language
,源语言)编写的程序,并将其翻译成另一种语言(target language
,目标语言)的 等价 (equivalent
) 程序。“等价” 意味着目标程序的行为与源程序完全一致。
目标语言可以是:
- 其他高级语言,如 C, C++, Java。
- 微处理器能理解的
machine language
(机器语言)。 - 领域特定语言,如打印机用的 PostScript。
编译器的一个重要功能是 错误处理 (
Error Handling
)。如果源程序有错,编译器必须检测并报告这些errors
。
2. 编译器的主要阶段 (Main Phases)
一个编译器通常被分为三个
- 分析 / 前端 (Analysis / Front-end): 负责理解源程序。
- 中间代码生成器 (Intermediate Code Generator): 在前端和后端之间架起一座桥梁。
- 综合 / 后端 (Synthesis / Back-end): 负责生成目标程序。
前端的三个阶段 (Analysis / Front-end)
前端的工作是将源代码分解并分析其结构和含义,它包含三个步骤:
3.1. 词法分析 (Lexical Analysis)
执行者:
lexer
或scanner
(词法分析器)。工作内容:
- 以字符流 (stream of characters) 的形式读取源程序。
- 将字符流分割成有意义的单元,称为
lexemes
(词素)。 - 将每个
lexeme
分类为一个token
(标记)。
- 这个过程类似于自然语言处理中的
tokenizer
,负责把句子切分成单词。 - 在多数语言中,
lexer
会跳过空格和注释。
使用工具: 通常使用
regular expressions
(正则表达式) 来定义tokens
的规则。例子: 对于 C 代码
num=1+23;
- Lexemes:
num
,=
,1
,+
,23
,;
- Tokens:
identifier
,ass_opt
,int_literal
,plus_opt
,int_literal
,semicolon
- Lexemes:
3.2. 语法分析 (Syntax Analysis)
执行者:
parser
(语法分析器)。工作内容:
- 使用一套
grammar
(语法) 规则来分析token
序列的结构。 - 将
tokens
组织成一个有层次的嵌套结构,称为parse tree
(语法分析树) 或syntax tree
(语法树)。
Parse tree
完整地展示了语法结构,而syntax tree
是其简化版本,更像一个表达式树 (expression tree
)。
- 使用一套
例子: 对于
num=1+23;
,其语法树 (syntax tree) 可能如下:1
2
3
4
5=
/ \
identifier(num) +
/ \
int_literal(1) int_literal(23)
3.3. 语义分析 (Semantic Analysis)
输入:
parse tree
或syntax tree
。工作内容: 检查程序的“含义”(
meaning
)是否正确。这是发现逻辑错误的阶段。检查项举例:
- 变量是否在使用前被定义 (
undefined variables
)。 - 变量类型是否匹配操作 (
types of variables
)。例如,不能把一个字符串赋值给一个整型变量。 - 变量是否被重复定义 (
multiple declaration
)。
- 变量是否在使用前被定义 (
此阶段还会执行隐式的
type conversions
(类型转换),例如在 C 语言中,将float
赋值给int
时,编译器会自动插入一个转换操作。
中间代码 (Intermediate Code)
目的:
- 前端产生的
parse tree
太高级,直接转换成低级目标代码很困难。中间代码是一个更易于处理的过渡形式。 - 解耦 (Decoupling): 中间代码充当了前端和后端之间的
interface
(接口)。这使得编译器可以轻松支持多种源语言和目标平台。例如,GCC
可以为 C, C++, Java 等多种语言生成不同平台(Windows, Linux)的代码,因为它共享了相同的中间表示。
- 前端产生的
本课程使用的类型:
three-address code
(三地址码)。它比工业界常用的Single Static Assignment (SSA)
更简单。三地址码特点: 每条指令最多包含一个操作符和三个地址(变量)。
例子:
id1 = id2 + id3 * 2;
会被翻译成:1
2
3
4temp1 = float(2)
temp2 = id3 * temp1
temp3 = id2 + temp2
id1 = temp3
后端的两个阶段 (Synthesis / Back-end)
后端负责将优化后的中间代码转换成最终的目标代码。
5.1. 代码优化 (Code Optimization)
目的: 改进中间代码,使其运行得
faster
(更快) 或体积smaller
(更小)。这是一个非常复杂的领域,有些优化可能需要数小时才能完成。
例子: 上面的三地址码可以被优化为:
1
2temp1 = id3 * 2.0
id1 = id2 + temp1这里将整数
2
提前转换成了浮点数2.0
,并减少了一个临时变量。
5.2. 代码生成 (Code Generation)
目的: 将优化后的中间代码翻译成目标语言(通常是汇编或机器码)。
包含三个核心且非常困难的任务 (都是
NP-hard
问题):- 指令选择 (Instruction selection): 为一个操作选择最合适的机器指令。例如,
x * 2
可以用乘法指令,也可以用更快的位左移指令x << 1
。 - 指令调度 (Instruction scheduling): 重排指令顺序以适应处理器的
pipelining
(流水线),从而提高执行效率。 - 寄存器分配 (Register allocation): 决定哪些变量在何时存放在 CPU 中极其有限但速度飞快的
registers
(寄存器) 中。
- 指令选择 (Instruction selection): 为一个操作选择最合适的机器指令。例如,
贯穿始终的组件 (Other Components)
这些组件在编译的各个阶段都会被调用。
符号表管理器 (Symbol Table Manager):
Symbol table
(符号表) 是一个数据结构,用于存储程序中所有identifiers
(标识符,如变量名、函数名) 的信息,例如它的类型 (type
)、作用域 (scope
) 等。- 前端用它来检查语义错误,后端用它来分配内存。
错误处理器 (Error Handler):
- 当发现错误时,一个好的编译器不会立即停止,而是会尝试
recover
(恢复),以便在一次编译中尽可能多地找出错误。
- 当发现错误时,一个好的编译器不会立即停止,而是会尝试
编译的全景图 (The Whole Picture)
在实际应用中,编译器只是工具链中的一环。
- 预处理器 (Preprocessor): 在编译前处理宏 (
#define
)和文件包含 (#include
)。它基本上是做文本替换。 - 编译器 (Compiler): 将预处理后的代码翻译成汇编代码。
- 汇编器 (Assembler): 将汇编代码转换成可重定位的
object code
(目标代码)。 - 链接器 (Linker): 将多个目标文件和库文件 (
libraries
) 合并在一起,解决地址问题,最终生成一个可执行文件 (absolute executable code
)。
Lecture Notes: Compiler Construction
https://tosakaucw.github.io/lecture-notes-compiler-construction/