章节目录

合抱之木,生于毫末

本节阅读量:

这一章只做一件事:让下面这段小语言程序完整走过解释和编译两条路径。

1
(+ 40 2)

解释器会直接计算并打印:

1
42

编译器会生成一段能返回 42 的 x86-64 汇编。把汇编交给 cc 后,我们还能得到真正的可执行程序。

这门小语言使用 Lisp 风格语法,(+ 40 2) 表示 40 + 2。第一章还允许单个整数,例如 42、-7;完整规则下一节会展开。

通过这个加法例子,我们会走过一门语言实现里最小、但完整的两条路:

1
2
源代码 -> token -> AST -> 解释器 -> 42
源代码 -> token -> AST -> IR -> 汇编 -> 可执行程序

第一章的目标不是“记住一堆术语”,而是先建立一个感觉:

1
2
程序不是一上来就能执行。
我们要先把文本读懂,再决定怎么执行它。

你需要先知道什么

你只需要会一点 C++ 基础:

  • 能看懂 struct。
  • 能看懂函数调用。
  • 大概知道指针或对象可以互相引用。
  • 知道递归函数会调用自己。

其他概念都会在用到时再解释。本章核心代码只有几百行,但 lexer、parser、解释器和编译器之间的边界已经完整存在。

代码架构

第一章代码很小,但已经按一门语言实现的基本结构分好了层。先看目录,不要一上来就钻进某个函数里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
code/01_numbers/src/
  cli/main.cpp              命令行入口,负责读文件、选择 run/ast/ir/compile

  front/ast.h               AST 节点定义:IntExpr、AddExpr
  front/ast.cpp             AST 节点的构造和 dump
  front/lexer.h/.cpp        source -> tokens
  front/parser.h/.cpp       tokens -> AST

  interp/interpreter.h/.cpp AST -> 整数结果

  compile/ir.h/.cpp         AST -> IR
  compile/assembly.h/.cpp   IR -> x86-64 assembly

这些英文名可以先这样读:

1
2
3
4
front     把源码读成 AST
interp    沿着 AST 直接算结果
compile   把 AST 变成 IR,再变成汇编
cli       把命令行和这些模块串起来

其中三个名字最重要:

1
2
3
token   源码小块,例如 "("、"+"、"40"
AST     abstract syntax tree,语法树,程序结构
IR      intermediate representation,编译器内部的草稿步骤

这一章最关键的分界线是 AST:

1
2
3
源代码 -> tokens -> AST --interp--> 整数结果
                      |
                      +--compile--> IR --> assembly

也就是说,lexer 先把文本切成 token,parser 再把 token 组装成 AST。解释器和编译器处理的是“结构问题”:一旦 parser 返回了 AST,后面的代码就不需要再关心源码里有几个空格、括号写在哪里、数字有几位。

本章会用四个命令观察同一个程序的不同阶段:

命令 观察到什么
run 解释器直接计算出的结果
ast parser 生成的语法树
ir 编译器生成的中间计算步骤
compile 写入 .s 文件的汇编文本

这里要特别区分:run 会把 42 打印到终端,编译后的可执行程序则把 42 作为退出码返回。第 0 章已经演示了怎样用 echo $? 查看它。

第一次读代码,可以按这个顺序:

1
2
3
4
5
6
7
1. front/ast.h              先看程序结构长什么样
2. front/lexer.cpp          再看源码怎么切成 token
3. front/parser.cpp         再看 token 怎么变成 AST
4. interp/interpreter.cpp   看 AST 怎么直接算出结果
5. compile/ir.h/.cpp        看 AST 怎么拆成 IR
6. compile/assembly.cpp     看 IR 怎么变成汇编
7. cli/main.cpp             最后回看命令行怎样把这些步骤串起来

这样读会比直接从 main 一路跳函数更稳:先理解每一层负责什么,再看它们怎么连起来。

汇编部分适合分成两步读,不要同时处理“汇编是什么”和“生成器怎样写汇编”:

1
2
07_assembly  先看懂寄存器、栈帧和栈槽怎样工作
08_compile   再把 IR、栈槽分配和生成器代码逐行对应

第一遍不需要记住整套 x86-64 指令,但要能说明当前栈帧怎样分配空间、怎样用负偏移保存临时变量,以及返回前怎样收回空间。这三件事是读懂第一章编译器所必需的。

读完这一章,你不需要掌握完整的编译原理。只要能回答三个问题,就已经抓住了主线:文本怎样变成 AST,AST 怎样被解释器求值,AST 又怎样经过 IR 变成汇编。


0.3 make:只在需要时重新构建

上一节

1.1 从两个表达式开始:整数和加法

下一节