在这篇文章中,我将介绍编译器的基本知识,并讲解编写编译器的5个主要阶段。
什么是编译器?
编译器、转译器和解释器是三个类似的概念。
编译器是一种程序,它将整个程序的源代码转换为计算机处理器可以执行的机器代码。
转译器用于将一种高级语言转换为另一种高级语言,例如将Java转换为Javascript。
解释器是一种直接读取并执行源代码的程序,不会创建可执行程序。
编译器流程
过去60年来,人们对语言和编译器进行了深入研究,并发展了理论和最佳实践。在许多编译器中,翻译发生在多个阶段:
Source code -> Parsing -> Semantic Analysis -> Optimization -> Code Generation -> Target code
Parsing
Parsing将源代码转换为更易于进行语义分析的数据结构,通常是一棵树(“语法树”)。
例如,a = 3 + sin(x) 的语法树如下
Assignment
/ \
a Expression Add
/ \
3 Function call
/ \
sin x
Parsing还检查源代码是否遵循源语言的语法。例如,“a 3 = +sin)x”会导致语法错误。
Lexing
通常,在解析之前还会进行另一个阶段,即Lexing。Lexing将源代码拆分为一系列符号,删除空格、换行。
Source code -> Lexing -> Parsing -> ...
它还检查简单的问题,例如在大多数语言中,不允许使用01a
作为标识符。Lexing应该处理这种类型的错误。
例如,a = 3 + sin(x)的符号如下:
- symbol 1: Identifier a
- Symbol 2: Assignment operator
- Symbol 3: Number 3
- Symbol 4: Arithmetic operator "+"
- Symbol 5: Identifier "sin"
- Symbol 6: Opening parenthesis
- Symbol 7: Identifier "x"
- Symbol 8: Closing parenthesis
语义分析
在这个阶段中,我们分析上一阶段生成的语法树。
了解代码中定义了哪些新类型、变量和函数 类型检查 流检查
更详细的例子:
- 变量“a”是否已定义?
- 表达式的类型是什么?
- sin()有多少个参数?
- “x”已经初始化了吗?
优化
这是实现具有良好性能的编译器的关键阶段。
例如:
- 在编译时预计算结果。
int x =3; int y = 7 * x; // -> int y = 21;
- 消除常见的表达式。
int x = 3 + z; int y = (3 + z) * 2; //-> int y = x * 2;
- 不变的代码。
for(int i = 0; i<10; i++){ int x = 3; } -> 将int x = 3移到循环外面。
代码生成
根据优化后的语法树,我们生成汇编或机器代码。
Tiny语言
我们学习了编写编译器的四个步骤,但这些都是概念性的,你可能希望自己编写一个编译器。我已经开始了一个Tiny
编译器项目。
我想编写一个简单的Tiny编译器。这个想法是源于我的一个作业,但是不允许在公共社区发布。作业的截止日期即将到来,所以我无法继续完成Tiny。到目前为止,我已经绘制了DFA并编写了Tiny的语法。有兴趣学习编写编译器的人可以继续完成它。如果您有问题,可以给我留言,我可以帮助你解答问题。为什么我这么好心?因为我希望这个项目能成为一个友好且对有兴趣编写编译器的人有用的项目。