python编译器开发0: 基础理论

本文最后更新于 2026年4月28日 下午

感觉很厉害的样子

前言

为了完成我的项目, 使用python写了一个编译器, 还写了篇论文, 不转载到博客实在太浪费了.

在论文中, 我实现了一种被a-level定义的伪代码, 虽然叫伪代码, 但这群人只是照着BASIC创造了一门垃圾的语言.

这系列博客出于教育目的, 但我不会非常深入探讨理论, 鲁迅曾经曰过: people should have b number, 对于学术理论, 机械工业出版社的编译原理(Compilers: Principles, Techniques, and Tools)是一个非常好的选择.

因此, 我会更加关注于编译器的实现层面, 但为了以后文章的描述方便, 仍然需要介绍一些基础理论.

编译过程

1. 编译流程

在本章中,我将通过伪代码介绍这个编译器的整个工作流程:

1
2
3
4
5
DECLARE A : INTEGER
A <- 1+2*3
IF A >= 5 THEN
OUTPUT A
ENDIF

选择这段代码的原因是它涵盖了伪代码的几个主要特点,包括表达式计算、变量处理、条件语句和跳转。因此,我们可以通过对这段代码的编译过程来了解编译器的详细工作流程。

1.1 词法分析

在编译过程的第一步中,词法分析器的主要任务是识别源代码中的标记,并将它们转换为内部表示。词法分析器通过将输入的源代码分解为单个标记来实现这一目标,标记是编译器中用于表示源代码中单个语义单元的最小单位。

每个标记由两部分组成:名称和属性值。<name, value>。标记的名称是在语法分析期间用于句法分析的抽象符号表示。标记的属性值可以是指向包含有关标记的附加信息的符号表的指针,或者它可以表示一个常量值。这些标记可以被分类为关键字、文字、操作符或标识符。

通过词法分析,词法分析器确保源代码遵循编程语言定义的预定结构。换句话说,词法分析器确保源代码由编程语言语法识别的有效单词和短语组成,从而减少语法错误的可能性。

经过词法分析器处理后,输入的源代码被转换为以下标记流:

1
2
3
4
5
<DECLARE> <id, ‘A’> <:><INTEGER>
<id, ‘A’> <<-> <num,1> <+> <num,2> <*> <num,3>
<IF> <id,0><>=><num,5><THEN>
<OUTPUT><id, ’A’>
<ENDIF>

1.2 句法分析

在输入的源代码经过词法分析器标记化后,解析器负责分析源代码的语法,并从标记流构建抽象语法树(AST)。句法分析的主要目标是确保源代码具有有效的结构,并识别代码中的潜在错误。

解析器使用一组规则,也称为文法,来确定标记流是否对应于有效的源代码。如果是,解析器构建表示源代码层次结构的AST。AST是一种类似树状的数据结构,其中每个节点表示源代码的语法元素,边表示这些元素之间的关系。

在提供的伪代码中,句法分析规则可以定义如下:

  1. 声明一个整数变量 A。
  2. 将变量 A 的值分配为 1+2*3。
  3. 如果变量 A 的值大于或等于 5,则输出变量 A 的值。

使用这些规则,解析器可以构建代表提供的伪代码结构的 AST。

表示伪代码结构的语法树示意图.png

此外,变量的声明不会向后传播,而是直接在语法分析阶段处理。具体来说,将在符号表中添加新的位置,以存储相应的变量。符号表是在程序语法分析和执行过程中用于存储变量内容的表格。通过符号表,变量可以通过符号表中的地址引用,从而实现更高的运行时效率和较低的程序复杂性。

1.3 语义分析

在语法分析之后,编译器执行语义分析,以确保源代码遵循编程语言的规则,且程序的含义是正确的。语义分析的主要任务包括类型检查、作用域检查和检查语义错误。

在提供的伪代码中,语义分析需要执行以下任务:

  1. 检查变量 A 是否已在当前作用域中声明。
  2. 计算表达式 1+2*3,确保操作数和运算符彼此兼容,并且结果是整数类型。该表达式的结果应与变量 A 的声明类型(整数)一致。
  3. 检查条件表达式 A>=5 是否为布尔类型,这是 IF-THEN 语句所需的。
  4. 检查变量 A 是否在 OUTPUT 语句中使用,确保 A 的类型是 OUTPUT 语句支持的类型。如果 A 的类型是布尔类型,则应报告类型错误。

如果任何语义检查失败,编译器必须报告语义错误,阻止程序运行。

例如,在提供的伪代码的第二行中计算表达式 1+2*3 后,结果应为整数类型,这与变量 A 的声明类型一致。如果 A 是不同类型的,编译器应报告类型错误,阻止程序运行。

1.4 中间代码生成

成功解析源代码并构建了 AST 之后,编译过程的下一个阶段是生成中间代码。这一阶段的目标是将 AST 转换为较低级别的表示:中间代码。

中间代码通常是源代码的简化版本,由一系列指令组成,这些指令可以在虚拟机中执行。

中间代码通常以字节码的形式表示,每个字节码由操作和操作数组成,格式为:

1
操作 操作值

所有字节码在数据栈上执行。数据栈支持两种操作:push 和 pop。push 操作将值存储到栈中,pop 操作从数据栈返回最近推送的值(即栈顶元素),然后将其从数据栈中移除。这个特性被称为 LIFO(后进先出)。

中间代码生成阶段涉及遍历 AST 并为树中的每个节点生成字节码。可以使用多种算法来完成这个过程,包括深度优先遍历和递归下降。

例如,在 AST 中,对于表示 * 的节点,它有两个子节点 2 和 3,首先会为 2 和 3 分别生成相应的字节码 load_constant 2load_constant 3。然后,对于 * 节点,会生成相应的字节码 arith_op *。对于这三个字节码,虚拟机首先将值 2 和 3 推入栈中,然后弹出这两个值进行相乘,将结果值 6 推入栈中。

通过语法分析生成的 AST,我们可以获得以下中间字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. L1: load_constant 1
2. load_constant 2
3. load_constant 3
4. arith_op *
5. arith_op +
6. store_id 0
7. L3: load_id 0
8. load_constant 5
9. compare_op 143
10. if_false_goto 2
11. L4: load_id 0
12. output
13. L2:

关于每个字节码的操作细节将在下一章关于虚拟机的章节中讨论。

1.5 中间代码执行

生成了中间代码之后,编译过程的下一个阶段是执行程序。这涉及使用虚拟机来执行在中间代码生成阶段生成的每条字节码指令。

虚拟机包含字节码解释器和数据内存。字节码解释器负责解码和执行每条字节码指令,而数据内存用于存储程序执行期间所需的值。

虚拟机对中间代码生成过程中生成的字节码执行以下操作:

  1. (L1)将常量值 1 推入数据栈。
  2. 将常量值 2 推入数据栈。
  3. 将常量值 3 推入数据栈。
  4. 执行乘法操作(arith_op ),栈顶元素变为 23 = 6。
  5. 接下来,执行加法操作(arith_op +),弹出顶部元素 6,然后弹出新的顶部元素 1,然后将结果 1+6=7 推入栈中。
  6. 最后,将这个结果 7 赋值给变量 A(store_id 0)。
  7. (L3)将变量 A 的值(load_id 0)7 推入栈的顶部。
  8. 然后将常量值 5 推入栈。
  9. 执行大于等于比较(compare_op 143),弹出元素 5 和 7 进行比较。
  10. 如果 7>=5 不成立,跳转到标签 2(第 13 行),否则继续执行第 11 行。
  11. (L4)将变量 A 的值(load_id 0)推入栈中,
  12. 然后打印栈顶元素 7(output)。
  13. (L2)最后一行结束程序的执行。

结语

这就是基础理论的内容, 你可以通过编译原理豆知识分类来查看其他文章, 其余章节将探讨各阶段的详细代码实现.


python编译器开发0: 基础理论
https://bainianlaoyao.github.io/2023/08/23/typecho-recovered-69-python编译器开发-基础理论/
作者
百年老妖
发布于
2023年8月23日
许可协议