Skip to content

这个编译器是怎么做的

这篇文档现在作为项目文档入口页,不再承担所有细节说明。

如果你想快速理解这个编译器,建议按下面顺序阅读:

  1. 整体架构与两条编译路径
  2. 词法分析与 Token
  3. 语法分析与 AST
  4. 语义分析与符号表
  5. IR 与 Lowering
  6. 优化器做了什么
  7. 后端与系统工具链
  8. 如何调试整条编译流水线

先抓住三个核心判断

1. 这不是“直接输出机器码”的编译器

这个项目第一代先输出规范化 C,再调用系统里的 clangcc 生成可执行文件。

这样做的目标不是偷懒,而是先把完整、可验证、可维护的编译链路打通。

2. 它不是一条完全统一的单路径编译器

当前实现有两条后端路径:

  • 基础 int 子集:AST -> IR -> 优化 -> 规范化 C
  • 扩展能力如 char、字符串、数组、指针:AST -> AST Backend -> 规范化 C

也就是说,扩展特性目前还没有全部进入统一 IR。

3. 这个项目更像工程化迭代,而不是一次性交作业

它的优先级很明确:

  • 先把链路做完整
  • 再把结构做清楚
  • 再保证每一层都可调试、可测试
  • 最后再继续向统一 IR、更强优化、原生后端演进

一眼看懂总流程

源代码
  -> Lexer
  -> Tokens
  -> Parser
  -> AST
  -> Semantic Analyzer
  -> IR Builder(仅基础 int 子集)
  -> Optimizer
  -> Backend C Generator
  -> clang / cc
  -> 可执行文件

如果程序使用了更高级的特性,例如 char、数组或指针,那么会跳过现有 IR 路径,改走 AST 直出后端。

不要把这条链当成“背完就算懂了的名词列表”

如果只是把上面这一串背下来,其实和背目录差不多,没什么用。

真正要看的,是每一层到底在帮下一层收拾什么:

  • 字符太散,就先切成 Token
  • Token 还是排成一长串,就整理成树
  • 树太高层,就继续拆成更像执行步骤的东西
  • 最后再交给后端和系统编译器

下面用一段极小的程序来串起来看:

int add(int a, int b) {
    return a + b;
}

1. 源代码

这一层只是普通文本,也就是字符序列。

编译器这时看到的本质上还是:

i n t 空格 a d d ( i n t 空格 a , ...

问题在于,后面的阶段不适合一直在原始字符上做判断。否则每一步都要重复问:

  • 这里是关键字还是变量名?
  • 这个 + 是运算符还是别的字符?
  • 这个 ) 到底属于哪一层结构?

所以第一步必须先切词。

2. Lexer

这一层对应源码文件:

  • src/c_core_compiler/lexer.py

它做的事情不是“理解程序”,而是沿着字符流往前扫,把能识别的片段切出来。

你可以把它想成:

  • 看见字母,就尝试扫一个标识符或关键字
  • 看见数字,就扫一个整数
  • 看见 '",就扫字面量
  • 看见 ==!= 这种双字符运算符,优先识别成一个整体
  • 看见空白和注释,就跳过

所以 Lexer 更像一个低头干活的扫描员,不是一个已经理解程序含义的人。

3. Tokens

这是 Lexer 的输出。

上面那段 add 代码在这个阶段,大致会变成:

KW_INT("int")
IDENTIFIER("add")
LPAREN("(")
KW_INT("int")
IDENTIFIER("a")
COMMA(",")
KW_INT("int")
IDENTIFIER("b")
RPAREN(")")
LBRACE("{")
KW_RETURN("return")
IDENTIFIER("a")
PLUS("+")
IDENTIFIER("b")
SEMICOLON(";")
RBRACE("}")
EOF

这一步一做完,后面就不用再对着字符堆一点点扒了,而是能直接处理“已经贴好标签的小零件”。

4. Parser

这一层对应源码文件:

  • src/c_core_compiler/parser.py

它做的事是:把一串 Token 按语法规则拼成结构。

例如它要识别:

  • 顶层这里是不是函数定义
  • 参数列表从哪里开始,到哪里结束
  • return a + b; 这里是不是一条返回语句
  • a + b 这里是不是一个二元表达式

所以 Parser 干的不是“再扫一遍”,而是“把这些零件按语法规则拼起来”。

5. AST

这是 Parser 的输出。

到了这一步,程序不再只是线性序列,而是一棵树。上面那段代码在概念上会变成:

Program
  FunctionDecl add
    params: a, b
    body:
      ReturnStmt
        BinaryExpr(+)
          Name(a)
          Name(b)

这就是 AST 最值钱的地方:它把“谁写在谁前面”变成“谁挂在谁下面”。

如果没有 AST,后面的语义分析和 IRBuilder 就要自己重新猜:

  • 哪些 Token 构成一个函数
  • 哪些 Token 构成一个表达式
  • 哪个 + 的左右操作数分别是谁

那会非常乱。

6. Semantic Analyzer

这一层对应源码文件:

  • src/c_core_compiler/semantic.py
  • src/c_core_compiler/symbol_table.py

这一层不再问“语法像不像”,而开始问“语义是否合法”。

比如它会检查:

  • ab 是不是已定义参数
  • 有没有重名参数
  • return 有没有返回值
  • 函数调用时参数个数对不对

也就是说,语法分析只保证“长得像程序”,语义分析才保证“这个程序在当前规则下说得通”。

7. IR Builder(仅基础 int 子集)

这一层对应源码文件:

  • src/c_core_compiler/ir_builder.py

它会把 AST 继续往低层展开,变成更偏动作的表示。

return a + b; 这种很小的语句,概念上会更接近:

t0 = load a
t1 = load b
t2 = binary + t0 t1
return t2

注意这里已经不再强调“这是一棵语法树”,而是在强调:

  • 先取什么值
  • 再做什么运算
  • 最后把什么返回

这就是 lowering 最接地气的意思:把结构拆成动作。

8. Optimizer

这一层对应源码文件:

  • src/c_core_compiler/optimizer.py

它不会重新发明程序逻辑,而是在已有 IR 上做保守整理。

例如:

  • 某个临时变量如果确定是常量,就直接传播
  • 某个表达式如果能提前算出来,就折叠
  • 某个分支如果根本走不到,就删掉

所以优化器不是来重新发明程序的,它更像是在整理桌面:

  • 明明能提前算出来的,就别留到运行时
  • 明明走不到的分支,就别继续占地方
  • 明明几个临时值能压一压,就别摊一桌子

9. Backend C Generator

这一层对应源码文件:

  • src/c_core_compiler/codegen/_portable_c.py

它的任务是把 IR 或 AST Backend 的结果,生成为规范化 C。

还是以 return a + b; 为例,最终后端输出不会再是 AST 或 IR,而会接近:

int add(int a, int b) {
    int r0, r1, r2;
    r0 = a;
    r1 = b;
    r2 = (r0 + r1);
    return r2;
}

这不是人手写时最顺眼的 C,但它非常适合:

  • 调试
  • 对照 IR
  • 交给系统编译器继续处理

10. clang / cc

这一层对应源码文件:

  • src/c_core_compiler/toolchain.py

这时已经不是项目自己的编译逻辑了,而是把生成的 C 交给系统编译器。

系统工具链负责:

  • 真正的 C 编译
  • 目标文件生成
  • 链接
  • 平台相关细节

所以这个项目第一代没有硬着头皮自己啃机器码、目标文件和链接器那一套,而是很务实地把这段活交给系统工具链。

11. 可执行文件

这是整条链路的最终产物。

这一步不是“文档里的抽象结果”,而是真正可以运行的程序。例如:

python3 -m c_core_compiler examples/hello.c -o build/hello
./build/hello

这里的 build/hello 就是整条链路的最终落点。

最关键的一句话

这条编译链不是“把代码重复表示很多遍”,而是每一层都在做一件很具体的事:

  • 字符太乱,就先切成 Token
  • Token 还是线性的,就组装成 AST
  • AST 还太高层,就降成 IR
  • IR 太粗糙,就做保守优化
  • 优化后的结果还不能直接运行,就生成规范化 C
  • 最后交给系统工具链变成真正二进制

理解了这一点,前面那串名词才不再只是名词。

这组文档的阅读方式

这套文档不是只讲“概念”,也不是只讲“代码文件名”,而是把两件事结合起来:

  • 编译原理里每一层在解决什么问题
  • 这个项目的代码到底是怎样实现这些层的

如果你是第一次看编译器,建议按顺序读。

如果你已经懂基础概念,只想快速对照代码结构,可以先看:

当前项目最值得注意的工程特点

  • 词法分析器和语法分析器都是手写的,控制流很直白
  • 语义分析明确依赖符号表做名字和作用域管理
  • int 子集有一条比较完整的 AST -> IR -> 优化 -> 后端链路
  • 扩展特性已经可用,但暂时走 AST 直出后端
  • CLI 支持 --emit-* 调试模式,适合逐层观察结果

一句话总结

这个项目最核心的价值,不是“它已经支持多少 C 特性”,而是“它已经把一个可解释、可验证、可演进的编译器骨架搭出来了”。