章节目录

本章总结

本节阅读量:

第一章只支持很小的语言:

1
2
整数,包括负整数
(+ expr expr)

但这已经走通了一门语言实现最重要的两条路:

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

前半条路回答:

1
这个程序是什么意思?

解释器沿着 AST 递归求值。遇到整数就返回整数,遇到加法就先求左右两边,再把结果相加。

后半条路回答:

1
这个意思怎样变成机器能执行的步骤?

编译器先把 AST 降成 IR,让嵌套表达式变成一行一行的计算步骤。再给每个临时变量分配栈槽:用 subq 移动 %rsp,为栈帧划出空间;用 %rbp 的负偏移稳定地读写每个栈槽;返回前再一次收回整块空间。

随后,编译器把 IR 翻译成 x86-64 AT&T 汇编。mini compile 到这里结束;离开编译器以后,我们再手动用 cc 把汇编变成可执行程序。

把两条路放在一起比较:

解释器 编译器
共同输入 AST AST
核心动作 递归求出每个节点的值 生成以后要执行的 IR 和汇编
mini 的输出 整数结果 .s 汇编文件
什么时候得到结果 解释时立即得到 汇编被链接并运行以后得到

两条路的过程不同,但必须实现同一套语言语义。对 (+ 40 2) 来说,解释器打印 42,编译后的程序也必须返回 42。

这一章里有几个分界线很重要:

1
2
3
4
5
6
lexer   只切 token,不理解程序含义
parser  把 token 组装成 AST
AST     保存程序结构
interp  按 AST 直接算结果
IR      保存更接近汇编的计算步骤
compile 把 IR 写成汇编文本,不负责调用 cc

后面章节会继续扩展语言,但不会推翻这条主线。第二章加入变量和 let 后,新的问题会变成:

1
2
解释器怎么知道变量的值?
编译器怎么把源码里的变量名变成栈槽?

也就是从“只计算数字和加法”,走向“处理名字和作用域”。


1.8 compile:把 IR 翻译成汇编

上一节

1.10 练习

下一节


本节目录