“若把源语言编译到SECD或其他抽象机,还需证明编译与机器执行保留源语言语义,或直接建立机器状态的类型不变量。源语言已经有进展与保持,并不自动证明任意闭包布局、栈操作或返回指令实现都正确。”
SECD 抽象机把函数求值拆成四类状态:已经算出的值放在 Stack,变量的词法绑定放在 Environment,接下来要执行的指令放在 Control,被函数调用暂时中断的现场放在 Dump。
它最值得理解的地方不是四个字母,而是一个调用如何发生:把调用者的未完成工作保存起来,进入闭包定义时的环境执行函数体,再把结果送回原来的计算位置。 这使闭包、词法作用域和调用返回成为可以逐步观察的状态变化。
形式陈述
本页固定哪一个版本
SECD 有多种等价或相关变体,指令顺序、栈方向和求值顺序并不统一。本页采用一个教学用的、带显式指令的版本:按值调用,先算函数再算实参,二元加法先算左边再算右边;栈顶统一写在列表左端。
源语言为
值是整数或词法闭包。闭包写成
机器状态写成
Landin 的原始呈现以及不同教材可能采用另一种操作数顺序。阅读时必须把编译规则、AP 的栈布局和返回规则作为一套检查;只调换其中一处,会得到一台行为不同甚至无法执行的机器。[1][2]
编译规则决定了栈应该长什么样
设
例如先编译函数、再编译实参,会使执行 AP 前的栈呈现
这不是凭习惯任选的顺序,而是由“栈顶在左、后算的值最后压入”推出来的。加法前的栈则为
顶层程序从 RTN,顶层没有;顶层成功结束于
AP 保存调用者现场,RTN 恢复现场并返回结果;函数体使用闭包保存的词法环境。
直觉
普通指令:局部改变当前现场
用 :: 表示把元素放到列表头部,普通指令的转移为
LDF 只建立闭包,不执行函数体。LD 查询的是当前环境,不扫描整个 Dump 去寻找“最近出现过的同名变量”。这两点共同保证了词法作用域。
本页语言没有静态类型检查,所以错误程序可能卡住:例如 LD 查询未绑定变量,ADD 遇到闭包,或 AP 遇到整数。实际解释器可以返回明确错误;数学上的部分转移关系则表示“没有适用规则”。这与程序无限执行是不同情况。
AP 与 RTN:进入调用,恢复调用者
函数应用规则是
它完成三件相互配合的事:保存调用者剩余栈、环境和控制;把函数体的当前栈清空;在闭包环境中加入形参绑定后开始执行。若使用调用时的
返回规则为
函数体正常完成时应恰好留下一个结果,因而这里写成单元素栈
Dump 保存的是“外层计算暂停在哪里”。当前 Control 保存的是“这一层接下来执行什么”。二者都与续延有关,但负责的层次不同;不能只留下一个函数返回地址,就认为已经保存了调用者所有需要的状态。
补充示意图
例子与边界
完整走一遍:为什么返回 3 后还要继续算
计算
记函数体代码
闭包
以下每行给出执行某一步后的状态要点,
| 已执行 | 当前栈 |
下一条指令 | Dump |
|---|---|---|---|
| 初始化 | LDC 1 |
||
LDC 1 |
LDF x B |
||
LDF x B |
LDC 2 |
||
LDC 2 |
AP |
||
AP |
LD x |
||
LD x |
LDC 1 |
||
LDC 1 |
ADD |
||
ADD |
RTN |
||
RTN |
ADD |
||
ADD |
无,结束 |
AP 后当前环境变为 RTN 恢复 [1] 和 [ADD],共同决定了程序还要把 1 加上去。
若实参本身也包含调用,求实参时又会出现新的 Dump 帧。每个帧恢复的是自己对应的外层计算,因此嵌套调用无需特殊的“找上一层变量”规则;统一的压入与弹出已经表达了控制嵌套。
词法环境:为什么答案是 11 而不是 21
考虑如下表面语法,let x=a in b 可以展开为
let x = 10 in
let f = (lambda y. x + y) in
let x = 20 in
f 1
建立 let x=20 创建了新的内层绑定,它改变的是调用处查找
因此 AP 进入
需要区分的是:词法绑定的保存,不等于可变对象的内容永远冻结。若语言后来加入引用或堆,闭包可以保存指向同一可变对象的位置;此时需要把 Store 纳入语义。不能把纯 SECD 环境的例子当作对可变语言的深拷贝规定。
为什么尾调用需要额外处理
本页的普通 AP 总会压入一个 Dump 帧,即使调用发生在函数体最后。尾递归程序因而仍可能不断积累只用于马上返回的现场;不能仅因为源语言写成尾递归,就断言这台基础机器使用常量调用栈。
要实现正确的尾调用行为,可以识别尾位置并复用调用者的返回续延,或引入专门的尾调用指令。它改变的是控制管理方式,不应改变源语言的求值结果。Danvy 对 SECD 的分解与重构分别讨论了求值顺序、尾递归以及其他机器变体,使这些差别可以从解释器结构中推导出来,而不是靠修改几条指令试出来。[2]
推论与应用
编译为什么与源语言求值一致
可以用一个“栈效果”不变量理解编译正确性:若表达式
常量和变量显然各压入一个值。加法先得到两个值,再消费它们并压入一个和,净效果仍是增加一个值。函数抽象压入闭包;应用先得到闭包与实参,再由 AP 隔离调用现场,函数体算出一个结果后由 RTN 恢复现场,整个应用最终同样只压入一个值。
这项结构归纳还需要环境与闭包解释之间的对应关系,才能成为完整的编译正确性证明。它至少明确了应该证明什么:是机器执行与源语言按值、词法作用域求值之间的模拟,而不是仅仅展示一段程序碰巧算对了。
机器中的 LDC、LD、闭包建立与调用现场管理,并不都对应源语言中的一次
与 CEK 以及解释器实现的联系
CEK 机器通常把待求值表达式、环境与续延作为状态,不必预先把整个表达式编译成这里的指令列表。SECD 的操作数栈与 Dump 则更直接地展示了值的暂存和调用现场恢复。
两者可以表达相关的按值求值语义,但它们的每个字段并不存在简单的一一替换关系。通过 CPS 变换、去函数化和闭包转换,可以解释不同控制表示如何从同一类求值器演化出来。
实现这台教学机器时,最有效的检查是同时测试:算术操作数顺序、函数与实参求值顺序、嵌套调用、词法遮蔽以及顶层结束。把这些与编译规则一起固定下来,SECD 就不再只是四个容器的示意图,而是一套完整可执行的求值机制。
SECD 是一种抽象机器,用显式状态分量描述求值过程,而不是直接规定物理处理器的指令。
参考资料
[1] Peter J. Landin, “The Mechanical Evaluation of Expressions,” The Computer Journal 6(4), 1964, pp. 308–320,SECD 与 applicative expression 求值的原始论文。
[2] Olivier Danvy, “A Rational Deconstruction of Landin’s SECD Machine,” BRICS Report Series RS-03-33, October 2003,原始机器、从左到右变体、尾递归和解释器分解。
[3] Peter Henderson, Functional Programming: Application and Implementation, Prentice Hall, 1980,SECD 风格的函数语言实现。