本章总结
本节阅读量:第四章真正增加的,不只是两种语法,而是“可调用的函数也可以作为普通值流动”:
|
|
前一个表达式创建函数,后一个表达式调用函数。函数可以先放进 let,也可以作为参数传给另一个函数,或者由另一个函数返回。无论写法怎样变化,都要分清创建和调用这两个时刻。
整章可以压缩成下面这张对照表:
| 源语言动作 | 解释器 | 编译器 |
|---|---|---|
lambda |
创建 Function(param, body) |
建立 IrFunction,把代码地址作为值 |
调用表达式 (callee argument) |
建立只含形参绑定的新环境 | %rdi 传实参,通过代码地址间接 call |
| 调用结果 | 函数体求出的 Value |
函数体结果放进 %rax |
| 一次调用的局部空间 | 一份调用环境 | 一份独立栈帧 |
语言:保存一段尚未执行的计算
求值 lambda 时,函数体不会运行;求值调用表达式时,才会依次求出 callee 和 argument,并用参数值执行函数体。
第四章的函数体只能使用:
- 自己的参数;
- 函数体内部
let建立的名字。
这条名字边界会同时约束解释器和编译器。源语言没有单独的 return;函数体表达式的值就是调用结果。
解释器:函数值和调用环境
Value 现在既能保存整数,也能保存 Function。本章的 Function 只记录参数名和函数体 AST,不保存创建 lambda 时的环境。
调用时,解释器先确认 callee 确实是函数,再建立一份新的环境,把 argument 绑定到参数名,然后在其中求值函数体。函数体内部的 let 可以继续扩展这份环境;调用结束后,这份环境随之消失。
因此,解释器里的核心对应关系是:
|
|
编译器:代码地址和独立栈帧
lowering 会把每个 lambda 的函数体登记成独立的 IrFunction。与函数值和调用有关的信息,则通过两类新增操作进入当前 ops:
|
|
到了 x86-64 汇编层,函数值就是入口标签的代码地址:leaq 取得地址,调用点把地址放进 %rcx、把参数放进 %rdi,再执行 call *%rcx。被调用函数建立自己的栈帧,把参数和临时结果放入自己的栈槽,最后用 %rax 交回结果,并用 retq 回到调用者。
这条编译路径可以压缩成:
|
|
本章的边界
下面的函数需要读取 lambda 外层的 x,第四章还不能表示它:
|
|
解释器的函数值没有保存外层环境,编译后的函数值也只有代码地址。等到闭包章节,函数值才会升级为“代码地址 + 捕获的环境”。本章还没有多参数、可变参数和 letrec 具名递归。
编译器也还没有带标签的运行时值,因此只承诺这样的程序:作用域符合本章规则;实际执行到的 +、eq? 和 call 使用正确种类的值;整数是本章示例中的小值且加法不发生有符号 long 溢出。解释器能报告 (42 1) 这样的错误,裸机器字编译器不能安全执行它。
这条语义边界和退出状态的观察边界是两件事。mini run 可以显示 <function> 或完整整数;手动链接后的 main 只能通过进程退出状态方便地观察 0 到 255 的整数。限制最终结果为小整数只是为了检查 $?,不是解释器与编译器语义一致的前提。
读完本章后,应当能够独立解释四件事:
- 为什么求值
lambda不会执行函数体。 - 为什么调用先求 callee,再求 argument,并建立新的参数环境。
- 为什么函数值在 IR 中同时对应
function_ref和独立的IrFunction。 %rdi、call、新栈帧、%rax和retq怎样完成一次往返。
完成主线后,章末还有一次独立的 C++ runtime 联调:把固定的 (cin)、(cout expr) 编译成外部直接调用,用正常标准输出观察完整整数。它只用于巩固 ABI 和链接,不由后续章节继承。