语言:名字由定义位置决定
本节阅读量:第七章没有增加语法,却改变了函数能够表达的程序。函数体现在可以读取 lambda 外层的绑定,并且在函数离开创建位置以后继续使用它们。
这项能力的语义核心是词法作用域:变量名的含义由源码中的定义位置决定,而不是由运行时从哪里调用函数决定。
创建位置和调用位置
先看本章最重要的例子:
|
|
addx 创建在第一个 x 的作用域里,所以函数体中的 x 指向 40。调用 addx 时,周围虽然又有一个值为 100 的 x,结果仍是:
|
|
如果函数体改从调用位置临时寻找 x,结果会变成 102。那是动态作用域,不是本书语言采用的规则。
求值 lambda 不会执行函数体
遇到:
|
|
解释器或生成代码会创建一个闭包。此时函数体还没有执行,参数 y 也还没有值。闭包先保存两类信息:
|
|
参数只在调用发生时加入,因此不属于提前捕获的内容。
调用顺序必须说清楚
调用表达式仍然只有一个 callee 和一个 argument。其求值顺序是:
- 先求值 callee;
- 确认 callee 是 closure;
- 再求值 argument;
- 从 closure 保存的定义环境开始;
- 若它是递归闭包,加入递归函数名;
- 加入本次参数绑定;
- 求值函数体。
可以把函数体使用的环境概括为:
|
|
调用位置的其他绑定不会整份混进来。
捕获的是 Value
本章的普通变量还不能被修改。闭包保存的是创建时查到的 Value:
|
|
这里保存的是整数 Value 40。
若外层变量保存的是 record,闭包捕获的是同一个 record 引用:
|
|
结果是 42。闭包 payload 中只放一个 tagged record pointer,不会复制或展开整个 record。
反过来,record 也能保存 closure:
|
|
结果同样是 42。这说明 Value 可以组成对象图,但不表示 record 和 closure 是同一种源语言值。
一个闭包可以创建另一个闭包
闭包可以作为函数结果返回:
|
|
结果是 42。求值过程分两次调用:
- 外层 closure 捕获
x = 40,收到参数y = 1; - 它创建并返回内层 closure,内层 closure 捕获
x = 40和y = 1; - 内层 closure 收到
z = 1,计算40 + 1 + 1。
外层调用结束以后,y 原来的栈帧已经不再执行,但内层 closure 仍然保存着对应 Value。这正是闭包必须使用堆对象的原因之一。
多个自由变量保持稳定顺序
下面的函数同时读取 x 和 y:
|
|
结果是 42。自由变量分析按第一次遇到的顺序得到 [x, y],创建闭包和函数体读取捕获槽都沿用这份顺序。顺序不会改变语言结果,却是编译器内部必须遵守的布局契约。
递归函数也可以捕获外层值
第五章的 letrec 只能依赖参数和自身。现在递归函数也能保存普通外层变量:
|
|
结果是 15。相对于 adddown 的函数体:
|
|
adddown 不会被写进自己的 payload。每次调用时,实际 callee 已经由隐藏的 closure 参数传入,函数入口直接把这个 tagged Value 绑定给 self 名字。这样既能递归,也不会为了表示递归而制造一个对象自环。
self 与参数同名仍按词法规则处理
普通 identifier 可以发生遮蔽。这个程序是合法的:
|
|
函数名和参数都写成 f。进入函数体后,参数绑定比 self 绑定更靠内,因此函数体中的 f 是参数,结果为 42,不会递归。
编译器不能只按源码字符串判断它们。lowering 会给 self 和参数分配不同的 IR 名字,再按词法环境查找最近的绑定。
closure 与 record 的种类边界
closure 和 record 都是 heap object,但 header kind 不同。下面的 predicate 是安全的:
|
|
结果是 0。
下面两个程序则是运行时错误:
|
|
|
|
解释器分别报告:
|
|
生成的程序会在读取 payload 前检查 header kind,失败时以状态 70 结束。
调用也检查对象种类
一个 record 不能被当作函数调用:
|
|
解释器报告:
|
|
编译后的 call 先检查通用 heap-object tag,再检查 HeapKind::closure。它不会把 record 的第一个字段误当成代码地址。
函数相等仍然没有定义
closure 成为堆对象,不表示语言自动获得函数相等语义。下面的程序仍然是错误:
|
|
解释器报告:
|
|
生成代码也会识别 closure operand,并以状态 70 结束。record 继续按对象身份参与 eq?;这两条规则不要混在一起。
语法总表没有新增分支
第七章继续使用第六章的表达式集合:
|
|
普通 identifier 仍然只是一个或多个英文字母。闭包能力来自实现语义的扩展,不来自新增关键字。
本章边界
第七章支持词法作用域下的普通闭包、嵌套闭包和能捕获外层值的 letrec。它仍然不支持:
- 修改变量;
- 多参数函数;
- closure 的源语言相等;
- 逃逸分析和栈上分配 closure;
- 主动释放对象或垃圾回收。
本章生成代码继续用 malloc 分配 closure。后续章节会改变可变变量和内存管理方式,但不会改变这里确定的词法作用域语义。