无名,天地之始;有名,万物之母

本节阅读量:

第一章里,程序只有整数和加法:

1
(+ 40 2)

第二章开始加入“名字”。我们希望能写:

1
(let x 40 (+ x 2))

解释器会直接打印:

1
42

编译器会生成一段能返回 42 的 x86-64 汇编。

这一次,多出来的关键问题是:

1
x 到底指向什么?

变量名不会自己保存值。解释器求值时,需要从环境里查:

1
x -> 40

编译器生成汇编时,需要把名字变成更低层的位置:

1
x0 -> -8(%rbp)

第二章的目标不是引入很多新语法,而是把“名字、绑定、作用域、位置”这条线走清楚。

从第一章延续什么

第一章已经跑通了这条主线:

1
2
3
source text -> tokens -> AST --interp--> integer
                         |
                         +--compile--> IR -> stack slots -> assembly

第一章已经有栈槽:IR 里的临时变量 t.0t.1 会被放到 -8(%rbp)-16(%rbp) 这样的栈槽里。

第二章仍然走同一条路,只是在 AST、解释器和编译器里各加一个概念:

1
2
3
source text -> tokens -> AST --interp with env--> integer
                         |
                         +--compile --> IR names -> stack slots -> assembly

这里新增的不是“栈槽”本身,而是这一步:

1
源码里的变量名 -> IR 里的内部名字

第一章的这些写法在第二章仍然有效:

1
2
3
4
42
-7
(+ 40 2)
(+ (+ 1 2) (+ 3 4))

第二章新增:

1
2
x
(let x 40 (+ x 2))

注意,单独的 x 在语法上是变量引用,但如果没有任何 let 绑定它,运行时应该报错。

代码变化在哪里

第二章目录结构和第一章一样,仍然是:

1
2
front -> interp
      -> compile

不需要重新讲一遍每个目录。真正变化集中在这几处:

1
2
3
4
5
front/ast.h/.cpp           增加 VarExpr 和 LetExpr
front/parser.cpp           把 identifier 和 let 读成 AST
interp/interpreter.cpp     增加 environment,用来查变量值
compile/ir.h/.cpp          把用户变量变成 IR 内部名字
compile/assembly.cpp       继续把 IR 名字分配到栈槽

lexer 在第二章基本不变。它仍然只负责把括号分开、按空白切 token;真正判断 x 是变量、let 是特殊形式的地方在 parser。

cli 也基本不变。runastircompile 这些命令仍然把同一条主线串起来。

读完本章后

你应该理解:

  • 为什么变量引用需要查找环境。
  • let 的 value 和 body 为什么使用不同的环境。
  • 同名遮蔽(shadowing)为什么不是修改旧变量,而是建立一个新的、更近的绑定。
  • 解释器里的 environment 和编译器里的内部名字、栈槽有什么区别。
  • 为什么编译后的汇编里没有用户变量名,只有栈槽地址。

1.10 练习

上一节

2.1 从名字开始:变量和 let

下一节