编程语言的自举:Java和C语言如何用自己的语言写出自己的编译器

从图灵的鸡蛋问题到GCC的诞生,从javac的演化到Go的真正自举——用最清晰的逻辑,完整讲述编程语言自举(Bootstrap)的概念、历史与完整过程。

编程语言的自举:Java和C语言如何用自己的语言写出自己的编译器

从一个哲学问题开始:先有鸡还是先有蛋

在编程语言的世界里,有一个和生物学类似的问题:

1
2
3
4
5
6
"一个语言的编译器,能不能用这个语言本身来写?"

如果可以:
  → 这个编译器需要编译自身才能工作
  → 但它还没有编译出来,怎么编译自己?
  → 这不就是"先有鸡还是先有蛋"吗?

这个问题的答案是:可以,但需要分两步走。

整个过程,在计算机科学里有一个专门的名字:自举(Bootstrap)


第一章:什么是自举——概念与核心原理

1.1 自举的正式定义

自举(Bootstrap):一种计算机语言的编译器或运行时系统,最初使用另一种语言(或其他工具)来构建,但最终能够用该语言自身来构建自己的完整实现。

1
2
3
4
自举的通俗定义:
  → 用自己的语言,写出自己的编译器
  → 然后用这个编译器,编译自己
  → 就像一个人用梯子爬到了房顶,然后把梯子建在了房顶上

1.2 为什么叫"Bootstrap"这个名字

这个词的来历非常有意思:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Bootstrap = 靴子上的鞋带(或者说"靴带")

来源:
  → 中世纪有一本奇书叫《莱茵河畔的巴霍芬》①
  → 书中描写了一个叫芒戈·德莱尼的人
  → 这个人从沼泽里拉出了自己的脚
  → 用靴子上的鞋带把自己从泥沼里拽了出来
  → 这个比喻后来演变成"bootstrapping"

引申含义:
  → 用现有的资源,把自己提升到一个更高的状态
  → 不需要外部帮助,从自身出发完成建设

计算机领域借用这个词:
  → 编译器在编译自己的时候,
    不需要"外部的完整编译器"来帮忙,
    只需要一个"足够启动的初始版本"就够了。
  → 这就是自举。

1.3 自举的三个层次

自举不是"有"或"没有"这么简单,它有三个层次:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
自举三层次:

层次1:部分自举(Partial Bootstrap)
  → 编译器的大部分用目标语言写成
  → 但最核心的部分(引导代码)仍然依赖外部语言
  → 例子:早期的Python解释器

层次2:完整自举(Full Bootstrap)
  → 编译器/解释器100%用目标语言写成
  → 不依赖任何外部实现
  → 例子:Go语言编译器

层次3:自展自举(Self-hosting Bootstrap)⭐
  → 用目标语言写编译器
  → 编译器自己编译自己
  → 编译出的结果和源码一致
  → 这才是计算机科学意义上的"自举"
  → 例子:GCC(C语言自举)、Rust编译器(rustc)

第二章:C语言的完整自举之路——从汇编到GCC

2.1 一切开始之前:没有C语言的时候

C语言的编译器,历史上是怎么诞生的?这是一段非常精彩的过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
时间线(简化版):

1969年:
  肯·汤普森 在 PDP-7 上用汇编写了 UNIX 操作系统
1970年:
  汤普森 用 B语言 重写了部分 UNIX
  (B语言 = BCPL的简化版,解释型,速度慢)
1972年:
  丹尼斯·里奇 在 B语言 的基础上创造了 C语言
  C语言 = 编译型 + 基本数据类型 + struct + 指针
1972年:
  里奇 用 B语言(解释型)写出了第一个 C语言 编译器
  这个编译器可以把 C代码 编译成 PDP-11 的汇编代码
1973年:
  里奇 用 这个C语言编译器,把 UNIX 操作系统
  用 C语言 重写了90%以上(里程碑事件)
1974年:
  里奇 开始把 C语言编译器 用 C语言 本身重写
  (即:用现有的C编译器,逐步重写编译器自身)
  这就是 C语言的 自举 过程

2.2 第一个C编译器的诞生(1972年)

关键洞察:第一个C编译器,不能用C语言来写,因为那时候C语言还不存在。

所以,里奇做了一个很聪明的事情:用B语言写第一个C编译器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
第一版C编译器的编写过程:

工具:B语言解释器(当时已存在)
材料:用B语言写一个编译器
产品:一个能把C代码翻译成汇编代码的程序

代码量:大约3000行B语言代码
编译过程:
  B语言解释器(已有)
    ↓ 解释执行
  里奇写的B语言程序 = C语言编译器
    ↓ 输出
  PDP-11汇编代码
    ↓ 汇编
  机器码
  运行机器码 = 一个可以工作的C语言编译器

这就是bootstrap的第一步:
"用已有的工具(B语言解释器),
  建造一个更强大的工具(C语言编译器),
  然后用这个更强大的工具,去建造更好的东西。"

2.3 C语言编译器的自举过程(1973-1977年)

有了第一个C编译器之后,关键的一步开始了:用C语言写C语言编译器本身。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
自举的关键逻辑(为什么可以这么做):

步骤1:现有工具
  → 有了一个能工作的C语言编译器(用B语言写的)
  → 记作:Compiler-v1(能编译C语言,但源码用B语言写)

步骤2:写新版本的编译器
  → 用C语言写一个新版本的编译器
  → 记作:Compiler-v2(C语言源码)
  → 这个编译器比v1更强大,但还没有被编译过

步骤3:用v1编译v2
  → 用Compiler-v1,编译Compiler-v2的C语言源码
  → v1能读懂C语言语法,所以它能编译Compiler-v2
  → 编译输出:Compiler-v2的可执行文件
  → 这个可执行文件 = Compiler-v2(用C语言写成,能工作)

步骤4:验证自举成功
  → 用Compiler-v2,再次编译Compiler-v2的源码
  → 如果两次编译结果完全一致(功能完全相同)
  → 说明自举成功!

这就是著名的"自展"过程。

2.4 为什么要用C语言重写C语言编译器

既然B语言版的C编译器能用,为什么还要花大力气用C语言重写?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
原因一:性能
  B语言是解释型的(运行时逐行翻译)
  C语言是编译型的(直接生成机器码)

  用B语言写的C编译器,速度很慢
  用C语言写C编译器,编译速度可以快5-10倍

原因二:可移植性
  B语言版的C编译器,只能在PDP-11上运行
  用C语言写C编译器,只要目标平台有C编译器,就能移植
  → 这是C语言可移植性优势的完美体现

原因三:自我改进
  C语言有struct,可以更好地组织编译器代码
  C语言有指针,可以直接操作内存,性能更好
  C语言有预处理宏,可以做代码生成优化
  用C语言写C编译器,编译器本身会变得更强

2.5 GCC的诞生——C语言自举的现代化

GCC(GNU Compiler Collection)是目前最著名的C/C++编译器集合。它的自举过程,是现代C语言自举的教科书级案例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
GCC自举简史:

1987年:GCC 1.0发布
  → 理查德·斯托曼(Richard Stallman)发起GNU项目
  → 最初的GCC用C语言写成(源码公开)
  → 第一步:用当时已有的C编译器(Portac)编译GCC的源码
  → 编译出的GCC,再用GCC自身编译一次(验证自举)

1989年:GCC 1.36
  → 完成了第一次完整的自举过程
  → GCC可以用自己的旧版本编译自己的新版本

1990年代:GCC 2.x → 3.x → 4.x
  → 编译器本身用C++重写(从GCC 3开始)
  → 支持更多语言:C++、Fortran、Java、Go等
  → 自举过程:每个版本都必须能用自己的前一个版本编译

2026年:GCC 15.x
  → 代码量超过1500万行
  → 支持17种编程语言
  → 每一次发布,都必须通过完整自举测试

2.6 GCC自举过程的完整流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GCC自举的现代完整流程:

源码仓库(git clone gcc.git)
第1步:用"引导编译器"编译GCC源码
  → 引导编译器 = 系统自带的GCC或Clang
  → 编译整个GCC源码树(用C和C++写成)
  → 产出:stage1编译器(可执行文件)
第2步:用stage1重新编译GCC源码
  → 这一步非常关键!
  → 用stage1编译器,再次编译GCC的所有源码
  → 产出:stage2编译器
第3步:对比stage1和stage2
  → 如果两者编译出的结果完全一致
  → 说明自举成功,编译器是"健康的"
  → 如果不一致(编译器bug),说明编译器本身有问题
第4步:stage3编译器(可选的第三次编译)
  → 进一步验证,确保编译器没有任何隐藏问题
第5步:通过所有测试套件(testsuite)
  → gcc/testsuite/ 下有数万条测试用例
  → 每一条都必须通过
  → 才能发布GCC的新版本

为什么需要两次编译?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
为什么要编译两次?一次不够吗?

答案是:一次确实不够,原因很微妙。

stage1编译器的源码 = GCC团队写的C/C++代码
stage1编译器 = 编译后的可执行文件

stage2编译器 = stage1编译器用GCC源码编译出来的

问题:如果stage1编译器有bug,
      它编译出的stage2编译器可能和正确的编译器不一样!

stage1的bug可能来自:
  → stage1编译器本身有隐藏的编译器bug
  → 或者编译优化有副作用

两次编译后对比:
  → stage2和stage1行为完全一致 → 编译器可信
  → stage2和stage1不一致 → 编译器有bug,需要修复

这就是GCC团队几十年坚持的"自举哲学":
"如果一个编译器连自己都编译不出稳定的结果,
  它怎么能编译出可信的程序?"

第三章:Java的自举——从C到Java再到Java

3.1 Java的"自举"问题,比C语言更复杂

Java的自举和C语言的自举有一个本质区别:

1
2
3
4
5
C语言的自举 = 源代码编译 → 可执行文件
(编译器是直接生成机器码的)

Java的自举 = 源代码编译 → 字节码 → JVM执行
(编译器生成的是中间字节码,不是直接可执行文件)

这意味着Java的"自举"至少涉及两个问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Java自举的两层问题:

问题1:javac编译器能不能用Java来写?
  → 答案是:可以,而且javac本身现在就是用Java写的
  → 但最初不是——最初javac是用C语言写的

问题2:JVM(Java虚拟机)能不能用Java来写?
  → 这个问题更复杂:
    → JVM是运行字节码的程序,需要编译成机器码才能运行
    → 如果JVM用Java写,Java编译器javac需要先存在
    → 但javac本身需要JVM来运行测试
    → 形成了鸡和蛋的问题

解决方案:
  → 最初版本的JVM + javac,用C语言开发
  → 然后用Java写一个完整的JVM(HotSpot JVM后来就是这么做的)
  → 然后用这个Java版JVM来运行Java程序
  → 这就是Java的"自举"

3.2 Java编译器的历史:从C到Java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Java编译器(javac)的自举历程:

1995年:Java 1.0发布
  → javac编译器:用C语言写成(由Sun公司开发)
  → 当时的Java编译器源码约2万行C语言代码
  → javac的输出:Java字节码(.class文件)
1997年:Java 1.1
  → javac开始部分用Java重写
  → 但最核心的代码生成部分仍用C语言
2004年:Java 5.0(代号Tiger)
  → javac的Java重写率达到90%+
  → 泛型、注解等新特性让编译器复杂度大幅增加
  → C语言版本难以维护,全面转向Java
2014年:Java 8
  → javac 100%用Java重写完成
  → 不再依赖任何C语言代码
  → javac本身也成为一个Java程序,可以在任何JVM上运行
现在(2026年)
  → javac的源码约70万行Java代码
  → 编译工具链(javac、java、jar)全部用Java写成
  → OpenJDK是完全开源的Java实现
  → 可以用任意版本的JDK编译OpenJDK源码本身(自举)

3.3 javac自举的详细过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
javac自举的完整过程(现代版本):

阶段1:初始状态
  → 你有一台安装了JDK的电脑
  → JDK里已经有javac(用旧版Java写的)
  → 你想要从源码编译新版OpenJDK
阶段2:Bootstrap JDK
  → 用电脑上已有的JDK作为"引导JDK"(Bootstrap JDK)
  → 这个引导JDK = javac + JVM + 核心类库
  → 你的电脑现在可以:写Java代码 → 用javac编译 → 运行
阶段3:编译OpenJDK源码
  → 下载OpenJDK源码(大约800万行Java代码)
  → 用Bootstrap JDK的javac,编译OpenJDK的所有源码
  → 这个过程会生成:
      ├─ 新的javac(新版本的Java编译器)
      ├─ 新的JVM(HotSpot,新版本的Java虚拟机)
      └─ 新的核心类库(rt.jar/java.base等)
阶段4:自举验证
  → 用新编译出的JVM,运行新编译出的javac
  → 用新的javac,再次编译OpenJDK源码
  → 对比两次编译的结果:
      ├─ 完全一致 → 自举成功 ✓
      └─ 不一致 → 有bug,需要排查

这就是JDK从源码构建的完整过程。

3.4 HotSpot JVM的自举——最复杂的自举案例

HotSpot JVM是Oracle JDK和OpenJDK的核心组件。它的自举过程,是Java生态中最复杂的自举案例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
HotSpot JVM自举的难点:

JVM的特殊性:
  → JVM本身也是一个程序(用C++和Java写成)
  → JVM需要编译成机器码才能运行
  → 但JVM是用来运行Java程序的

自举的鸡和蛋问题:
  → JVM是用C++和Java写的
  → C++部分:用g++编译成机器码(直接可执行)
  → Java部分:需要javac编译成字节码,然后JVM执行
  → 但JVM本身还没编译出来!

解决方案——两阶段构建:
  第一阶段:用C++编译器(g++)编译JVM的C++部分
    → 产出:JVM的C++部分(机器码)
    → 此时JVM的Java部分还没编译

  第二阶段:启动不完整的JVM(只有C++部分)
    → 用javac编译JVM的Java部分
    → javac是Java写的,需要JVM来运行...
    → 但此时JVM还不完整...

实际解决方案(Bootstrap JVM + C1/C2编译器):
  → 用已有的完整JDK来引导编译过程
  → Bootstrap JDK = 你的电脑上安装的JDK
  → 用Bootstrap JDK的javac,编译HotSpot的Java源码
  → 用Bootstrap JDK的JVM,运行HotSpot的测试套件
  → 用Bootstrap JDK的JVM + HotSpot的C++部分,组合成新JVM
  → 最终:用新JVM来运行新javac(自举验证)

3.5 GraalVM——Java自举的新里程碑

GraalVM是Oracle开发的新一代Java虚拟机,有一项令人惊叹的特性:可以用Java编写整个虚拟机的核心

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
GraalVM的意义:

传统JVM(HotSpot):
  → 核心部分(解释器、JIT编译器、GC)用C++写成
  → 只有Java标准库用Java写成
  → JVM本身的开发,仍然依赖C++工具链

GraalVM:
  → 核心部分(Graal编译器)用Java写成
  → 可以"提前编译"(AOT编译)成机器码
  → 编译后的Graal VM = 纯机器码程序,不需要JVM
  → 但Graal编译器本身 = Java程序

这形成了一个完整的循环:
  Java源码
    ↓ (用Graal的旧版本编译)
  Graal编译器(Java字节码)
    ↓ (用JVM运行)
  编译其他Java程序
    ↓ (用Graal的AOT编译)
  Graal VM(机器码)
    ↓ (运行)
  编译Java源码(包括Graal编译器本身!)
    ↓ (完美自举!)

GraalVM是目前已知的最优雅的Java自举实现之一。

第四章:其他语言的自举——百花齐放

4.1 Python——从C到Python到Python

Python的自举历史非常清晰,但有一个有趣的限制:Python本身无法完全自举

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Python自举的真相:

Python的解释器(CPython)是用C语言写的。
Python语言本身无法"用自己的语言解释自己"(Python是解释型语言)。

自举路径:

第一步:构建最初的Python
  → 1991年,Guido van Rossum用C语言写了Python解释器
  → Python解释器 = 一个C语言程序
  → 这个C语言程序可以解析Python语法、执行Python代码

第二步:Python标准库
  → Python的大部分标准库(os, sys, json, re等)用Python写成
  → 这些库可以被Python解释器直接执行

第三步:编译器(编译Python为字节码)
  → Python代码 → Python编译器(内置于解释器)→ 字节码(.pyc)
  → 字节码在PVM(Python虚拟机)中执行
  → PVM = Python解释器的核心,用C语言写成

为什么Python不能完全自举?

因为Python解释器本身必须用C/C++写成:
  → 解释器需要做IO操作(读文件、网络通信)
  → 解释器需要做内存管理(垃圾回收)
  → 这些底层操作在操作系统层面,只能用C语言调用

类比:
  → 就像"英语语法书"不能自己印刷自己一样
  → 印刷这本语法书的印刷机,不是用英语做的
  → Python解释器 = 印刷机(用C语言做成)
  → Python源码 = 印刷内容(用Python写)

4.2 Go——真正的自举典范

Go语言(Golang)是自举做得最漂亮的主流语言之一。Go编译器最初用C语言写,后来用Go完全重写,现在Go的整个工具链都是用Go自己写的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
Go自举的完整历程:

2009年:Go 1.0之前
  → Go编译器(gc)用C语言写成
  → Go语言的第一个版本:编译器用C,标准库用Go
  → 编译工具链(go build, go install等)用Go写成

2014年:Go 1.4(里程碑版本)
  → Go编译器gc从C语言完全重写为Go语言
  → 这是Go语言的"真正自举"时刻
  → 编译器本身不再依赖任何C语言代码
  → 自举过程:
      用Go 1.3的编译器编译Go 1.4的源码
      Go 1.4编译器(用Go写成)
      用Go 1.4编译器重新编译自己
      验证:两次编译结果完全一致 → 自举成功!

Go 1.4之后:持续自举
  → 每次Go版本发布,都用上一个版本编译
  → Go 1.5 → 用Go 1.4编译
  → Go 1.6 → 用Go 1.5编译
  → Go 1.20 → 用Go 1.19编译
  → 2026年最新版本,全部工具链100%用Go自举

Go自举的完整工具链:
  go build        → Go编译器(gc,Go语言写成)
  go run          → 编译并运行
  go fmt          → 代码格式化(Go语言写成)
  go vet          → 代码检查(Go语言写成)
  go test         → 单元测试框架(Go语言写成)
  go doc          → 文档生成(Go语言写成)
  go generate     → 代码生成(Go语言写成)
  golangci-lint   → 代码质量检查(Go语言写成)

全部工具链 = 100%用Go语言写成 → 完美自举 ✓

4.3 Rust——最难的自举之一

Rust语言的自举在所有语言中可能是最复杂的,因为Rust有一个独特的特性:所有权系统(Ownership),这个特性本身需要一个编译器来实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Rust自举的特殊挑战:

Rust的编译过程分三个阶段(三个编译器):

第一阶段:rustc Stage 0(引导编译器)
  → 这是一个"提前构建"的rustc
  → 用其他语言(历史上用OCaml,现在用编译好的rustc)
  → Stage 0编译器用于编译Stage 1的编译器源码

第二阶段:rustc Stage 1(中间编译器)
  → 用Stage 0编译rustc的源码
  → Stage 1编译器的大部分功能已经完整
  → 但它的部分标准库依赖于Stage 0编译出的库
  → 实际上这个版本已经可以编译Rust代码

第三阶段:rustc Stage 2(最终编译器)
  → 用Stage 1编译器再次编译rustc的源码
  → 产出最终的rustc
  → Stage 2 = 100%用Rust语言写成且自举的编译器

为什么需要Stage 2?
  → 因为Stage 1编译器的自举过程可能引入bug
  → Stage 2编译器的编译结果应该与Stage 1完全一致
  → 如果不一致,说明编译器有问题

Rust的三阶段编译是Bootstrap中最复杂的之一:

  Stage 0(OCaml/旧rustc)
    ↓ 编译rustc源码
  Stage 1编译器
    ↓ 再次编译rustc源码
  Stage 2编译器(最终版)
    ↓ 对比Stage 1和Stage 2
    → 完全一致 → 自举成功 ✓

4.4 JavaScript引擎——浏览器中的自举

V8(Chrome的JavaScript引擎)的自举也很有特点。V8用C++写成,但它的"自举"过程在JavaScript运行时层面有自己的故事。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
JavaScript的自举——不同的层次:

层次1:JavaScript引擎(V8/SpiderMonkey)的自举
  → V8 = C++程序,可以编译成机器码
  → V8引擎用C++编译成机器码 → 这不需要JavaScript自举
  → JavaScript引擎本身不需要"用JS写JS引擎"

层次2:JavaScript运行时的自举
  → JavaScript的全局对象(Math, JSON, Array等)
  → 这些全局对象在V8中由C++实现(快速路径)
  → 但也可以由JavaScript代码提供基础实现

层次3:Node.js的自举
  → Node.js = V8引擎 + C++写的系统绑定(libuv, http parser等)
  → Node.js的核心模块用JavaScript写成
  → 这些JavaScript模块在Node.js启动时被加载执行

Node.js启动时的自举过程:
  boot.js(JavaScript)
    ↓ 加载
  node.js(JavaScript)
    ↓ 加载
  模块系统初始化(JavaScript)
  用户代码执行(JavaScript)

4.5 Haskell——自举的极端案例

Haskell编译器的自举可能是所有语言中最长的链之一。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Haskell自举链(惊人的长度):

Haskell编译器:GHC(Glasgow Haskell Compiler)

GHC自举历史:

阶段1(1990年代初):
  → GHC最初的版本用Lisp写成
  → GHC的输出:Haskell代码
  → 用其他Lisp系统来运行GHC

阶段2(1990年代中):
  → GHC用Haskell重写(早期版本)
  → 用旧的Lisp版GHC编译新Haskell版GHC
  → 自举:Haskell → Haskell编译器 → Haskell程序

阶段3(2000年后):
  → GHC编译器本身越来越复杂
  → GHC需要用GHC自身来编译
  → GHC的代码量:超过100万行Haskell代码

为什么Haskell自举特别复杂?
  → GHC依赖很多外部库(base, ghc-prim, template-haskell等)
  → 这些库本身也是GHC的一部分,需要一起自举
  → 自举顺序:必须严格按照依赖顺序编译

GHC自举的依赖链:
  ghc-prim(基本类型)→ integer-gmp(大整数运算)
    → base(核心库)→ template-haskell(模板元编程)
      → hadrian(构建系统)→ 用户代码

这条依赖链必须一字不差地按顺序编译。

第五章:自举的深层意义——为什么这很重要

5.1 自举的工程价值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
自举的四个工程价值:

价值1:证明语言的完整性
  → 如果一个语言能写出自己的编译器(至少大部分),
    → 说明这个语言有足够强的表达能力
    → 说明这个语言的抽象能力足够丰富
  → 这是对语言设计的一种最强验证

价值2:不依赖外部工具链
  → 如果语言依赖另一个语言的编译器来构建自己,
    → 那这个语言就永远受制于人
  → 自举后,语言可以完全独立地构建自己

价值3:自我测试与质量保证
  → 如果编译器能稳定地编译自己,
    → 说明编译器的实现是正确的
    → GCC的"两次编译对比"就是最好的测试
  → 任何编译器bug都会在自举过程中暴露

价值4:跨平台移植的基础
  → 假设你想把C语言移植到一台新的CPU上:
    → 你需要有一个C语言编译器
    → 如果C语言编译器用C语言写成,你需要先有一个C编译器
    → 这就是经典的"鸡和蛋"问题
  → 解决方案:用其他语言(更老的C编译器或汇编)
    写第一个C编译器 → 然后自举 → 然后移植

5.2 自举的语言能力阶梯

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
自举是一个语言"成熟度"的最重要标志之一:

阶梯1:能用其他语言写出编译器
  → 说明语言表达能力够用
  → 很多脚本语言(Perl、PHP)停留在这个阶段

阶梯2:能用自身写出大部分编译器
  → 还需要少量汇编或C语言辅助(引导代码)
  → Java的javac达到这个水平

阶梯3:能用自身写出完整编译器,并能自举
  → Go、Rust、Python(解释器核心部分)达到这个水平

阶梯4:编译器和运行时都能自举,且多平台完全一致
  → GCC、Clang(LLVM)、Go达到这个水平
  → 这是工程能力的最高体现

5.3 没有自举的语言——也有自己的精彩

不是所有语言都需要或应该自举。这个问题有重要的权衡:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
需要自举的语言(系统级语言):
  → C、C++、Rust、Go
  → 用途:操作系统、编译器、数据库、驱动
  → 这些场景需要最大程度的控制权,不需要外部依赖

不需要自举的语言(应用级语言):
  → Python、Ruby、PHP、JavaScript
  → 用途:Web开发、脚本、数据分析
  → 这些场景依赖外部运行时(C语言写的解释器/JVM/JS引擎)
  → 硬要自举反而增加复杂度,没有意义

自举是手段,不是目的:
  → C语言自举是因为它本身就是系统级语言
    需要最大程度的控制和可移植性
  → Python不追求自举是因为它的定位就是"胶水语言"
    依赖C语言解释器是合理的设计选择
  → Go追求自举是因为它要成为一个完全独立的语言工具链
    不依赖任何外部编译器的Go,才是真正的Go

第六章:自举的完整流程图——五种语言的横向对比

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
五种语言自举流程横向对比:

语言:C语言
引导工具:汇编语言(最早)/ 其他C编译器
自举过程:用旧C编译器编译新C编译器源码 → 自举验证
自举完成度:100%(GCC 1.0起完全自举)
当前状态:GCC、Clang均可自举,完美闭环

语言:Java(javac)
引导工具:上一个版本的JDK
自举过程:Bootstrap JDK编译OpenJDK源码 → 新JDK → 自举验证
自举完成度:100%(Java 5.0+)
当前状态:OpenJDK完全用Java自举

语言:Go
引导工具:上一个版本的Go编译器
自举过程:Go 1.3编译器编译Go 1.4源码 → 自举验证 → 后续版本迭代
自举完成度:100%(Go 1.4+完全用Go自举)
当前状态:Go全部工具链(go build/go test/go fmt)100%自举

语言:Rust
引导工具:上一个版本的rustc
自举过程:Stage 0 → Stage 1 → Stage 2(三阶段)→ 对比验证
自举完成度:100%(Rust 1.0+)
当前状态:rustc + Cargo(Rust包管理器)均可自举

语言:Python
引导工具:C语言解释器(CPython)
自举过程:Python语言无法自举,解释器必须用C写成
自举完成度:0%(无法自举)
当前状态:Python解释器(CPython)必须用C语言维护
           但标准库100%用Python写成

对比总结:
  → C语言:第一个真正自举的语言之一
  → Java:从C到Java,javac完成了优雅的转型
  → Go:现代语言中自举最完整、最清晰的案例
  → Rust:自举最复杂,但验证最严格
  → Python:选择了不同的道路(用C作为底层runtime),也是合理的设计

总结:自举的本质

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
这篇文章的核心观点:

自举的哲学意义:
  → 自举是一种"用自己的力量把自己提升"的隐喻
  → 从里奇用B语言写C编译器,到Go团队用Go重写Go编译器,
    自举展示了语言的自我进化能力

自举的技术价值:
  → 证明语言表达能力的完整性
  → 实现语言工具链的独立自主
  → 编译器的最高质量保证机制
  → 跨平台移植的必要条件

自举不是万能的:
  → 自举与否是设计选择,不是语言优劣的判断标准
  → Python不追求自举,但Python改变世界的程度不亚于C语言
  → 自举是一种工程哲学,不是语言的宿命

最重要的是:
  → 理解了自举,就理解了整个编译原理最精髓的部分之一
  → 编译器能编译自己,这个事实本身就是计算机科学最优雅的成就

关联文章:

  • 《万物基于C语言:丹尼斯·里奇与C语言帝国的兴起与永恒》——C语言的设计哲学与历史
  • 《为什么Kubernetes需要Istio:新手的灵魂拷问》——云原生时代的语言工具链
CC BY-NC-SA 4.0
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计