章节目录

第四章:函数成为值

本节阅读量:

前三章里,每个表达式最终都会算出一个整数。第四章加入一种新的值:函数。

这一章会一直使用下面的程序:

1
2
(let inc (lambda x (+ x 1))
    (inc 41))

先看它在语言层面的动作:

1
2
3
4
(lambda x (+ x 1))   创建一个函数值,暂时不执行函数体
let inc ...          把这个值绑定到名字 inc
(inc 41)             取出函数值,把 41 交给它
(+ x 1)              在 x 为 41 的环境里执行,得到 42

这里没有“函数声明”这种额外机制。lambda 本身就是表达式,它产生的函数可以放进 let,也可以作为参数和返回值继续传递。

本章仍然保留前面已经跑通的两条路线:

1
2
源码 -> AST -> 解释器 -> 值
源码 -> AST -> IR -> x86-64 AT&T 汇编

这一章的信息比前三章多,适合分两个阶段学习:

1
2
3
第一阶段:00—04,先看懂 lambda 创建值、call 使用值、解释器建立调用环境
第二阶段:05—07,再跟踪函数体怎样进入 IR、参数怎样进入新栈帧、call 怎样返回
可选收尾:10,把 cin/cout 编译成对 C++ runtime 的直接调用,再完成链接

第一阶段只要抓住“创建函数”和“调用函数”是两个时刻,不必提前记住 %rdi、%rax 或临时变量编号。

不过,两条路线里的“值”现在既可能是整数,也可能是函数:

1
2
语言中的函数值     解释器中的 Function     编译器中的函数入口地址
语言中的调用       新建参数环境并求函数体   按调用约定执行间接 call

先把这种对应关系留在脑中即可。后面的几节会分别把它落实到 AST、解释器、IR、栈帧和汇编,不需要在这一页提前记住所有细节。

先运行贯穿示例

程序已经放在 examples/function_value.lang:

1
2
3
4
5
cd code/04_functions
make
./mini ast examples/function_value.lang
./mini run examples/function_value.lang
./mini ir examples/function_value.lang

AST 是:

1
Let(inc, Lambda(x, Add(Var(x), Int(1))), Call(Var(inc), Int(41)))

解释器输出:

1
42

IR 第一次把主程序和函数体分开放置。这里先观察整体形状,不必理解每个临时名字的编号:

1
2
3
4
5
6
7
8
f.0 = function lambda0
inc0 = f.0
t.1 = call inc0 41
return t.1
lambda0(x):
  t.0 = x + 1
  return t.0
end lambda0

这里先注意三件事:

  • function lambda0 把一段函数代码的入口变成值。
  • inc0 = f.0 和整数的 let 一样,只是在保存一个值。
  • call inc0 41 到运行时才取出被调用的函数值,通过它进入 lambda0。

这一页先验证到 IR。完整的汇编、手动链接和退出状态检查会在栈帧与汇编两节走通;章末还有一次独立的 C++ runtime 联调,用 cout 直接显示完整结果。

本章增加的部分

1
2
3
4
5
6
src/front/ast.h/.cpp          LambdaExpr 和 CallExpr
src/front/parser.cpp          lambda 特殊形式和普通调用
src/interp/interpreter.cpp    函数 Value、参数环境和函数体求值
src/compile/ir.h/.cpp         function_ref、call 和独立函数 IR
src/compile/assembly.cpp      函数标签、代码地址、传参和间接调用
src/runtime/io.h/.cpp         章末可选实验的 C++ 输入输出函数

整数、负整数、加法、变量、let、eq? 和 if 都会继续保留。新功能不是另起一套语言,而是让已有表达式可以出现在函数体和调用位置中。

这一章刻意停在哪里

第四章只实现单参数、且不读取外层局部变量的函数。它还没有第五章的 letrec 具名递归构造。环境捕获要等闭包章节;本章的编译器也还没有运行时类型标签和类型检查,因此编译路径只使用实际执行路径上值种类正确的小整数程序来讲解。

读完本章后,你应该能沿着贯穿示例说明:

  • lambda 为什么不会立刻执行函数体。
  • 函数怎样被保存、传入、返回和调用。
  • 解释器为什么为一次调用建立新环境。
  • 编译器怎样把函数变成地址,再通过地址调用它。
  • 主程序栈帧和函数栈帧怎样在调用期间各自保存数据。

3.9 练习

上一节

4.1 语言:创建函数,再调用函数

下一节