“STLC 是静态语义与动态语义相互作用的最小实验场。进展与保持定理说明良类型闭项不会因类型错误卡住,强正规化定理另外证明每条完整 β 归约路径都有限。后者严格强于某个求值策略终止。”
类型系统给程序贴上的类型标签,只有与运行规则配合时才具有实际含义。进展定理保证一个良类型的闭程序不是已经得到结果,就是还能继续执行;保持定理保证执行一步后,它仍然具有原来的类型。
两者合起来建立了一条不变量:从良类型程序出发,执行过程不会走到“还不是结果、却没有任何规则可用”的状态。这里的关键不是程序一定结束,而是每一步始终留在类型系统允许的计算世界中。
形式陈述
固定语言,才有可以证明的命题
以下使用带布尔值的简单类型 λ 演算,按值、从左到右求值。类型与项为
值只包括
一个 λ 抽象已经是值,即使它的函数体还没有执行。变量不是值;应用和条件表达式也不是值。这些选择属于动态语义的一部分,换成按名求值或在 λ 内归约时,进展证明的具体分支也要改变。
类型环境
类型规则与执行规则如何配合
变量从环境中查找类型,布尔常量具有 Bool 类型。函数抽象与应用的规则是
应用规则把“函数接受什么”与“实参提供什么”对齐。条件表达式则要求条件是布尔值,两个分支具有同一类型:
小步动态语义中,应用先求函数,再求实参:
当两者都已经是值,且函数是 λ 抽象时,执行按值
条件表达式先归约条件;条件成为布尔值后,选择对应分支:
没有规则会把 true 当作函数调用,也没有规则把一个 λ 抽象当作布尔条件。这些就是类型系统必须防止良类型程序抵达的错误形状。
两个定理的准确表述
进展。 若
保持。 若
这里保持定理允许一般环境,进展定理则针对闭项。自由变量
一个没有下一步的项叫正规形。值是成功的正规形;一个不是值的正规形叫卡住的项(stuck term)。例如 true false 是闭项、不是值、也无规则可走,但类型系统不会给它合法的应用类型。
这组区分很重要:进展不是“程序永远能继续”,因为一个值已经可以结束;类型安全也不是“所有正规形都能运行”,而是良类型闭程序的正规形必然属于语言认可的值。[2]
直觉
进展证明:值的类型必须决定它的形状
证明先用到规范形引理:若闭值 Bool,则 true 或 false;若其类型为
引理通过检查值的语法和类型规则即可证明。它的作用是把静态判断“这是一个函数类型”,转换成动态信息“这里确实有一条 λ 应用规则可用”。只知道“子项已经是值”,还不足以完成这一步。
接着对
对应用
最后一种情形是两者都已是值。此时规范形引理把函数值确定为 λ 抽象,于是
对 if,若条件还能归约,就在条件位置前进一步;若条件已经是值,规范形引理保证它是 true 或 false,因此总能选出一个分支。分支在被选择前无需预先执行,这与本页的求值顺序一致。
保持控制每一步后的类型;进展排除“不是值、也不能继续”的卡住状态。这张图不把类型安全等同于必定终止。
保持证明:替换实参为什么不会破坏类型
保持证明的核心不是所有情况都复杂,而是
在绑定变量已经适当重命名、
则
这个结论准确匹配应用类型规则:函数体原本允许在
替换必须避免变量捕获。例如把自由变量
现在检查各种运行规则。对
经类型规则的反演,得到函数体与实参恰好满足替换引理的两个前提,故归约结果
对应用内部的归约,归纳假设说明被归约的子项保留原类型,再重新应用函数应用的类型规则即可。对条件内部的归约,同理保持条件的 Bool 类型;对选分支的规则,两个分支本来都已被要求具有同一个
这也解释了为什么 if 类型规则必须检查两个分支,而不能只检查某次运行碰巧会执行的那一个:保持性是一项对所有合法输入和所有允许归约步骤的统一陈述。[1]
补充示意图
例子与边界
两个反例说明为什么需要两条定理
假设保留原来的类型规则,却从动态语义中删掉
反过来,假设 if 的错误类型规则只检查条件和真分支,而忽略假分支。那么
会被错误地赋予 Bool 类型。它仍能按照条件规则前进一步,但结果是函数类型的 λ 抽象,原类型没有保持。这里运行规则并不缺失,问题出在静态规则没有约束所有可能被选中的结果。
两种错误对应不同层次:进展检查良类型的形状是否有动态规则接住;保持检查动态规则的结果是否仍留在良类型集合里。把其中一个定理当作另一个的替代品,会漏掉一整类设计缺陷。
类型安全不等于终止,也不自动等于全部正确性
本页的纯简单类型 λ 演算还有强规范化性质,但那是另一项定理,不能从进展与保持直接推出。若向语言加入具有合适类型规则的一般递归,仍可保持类型安全,却出现永不结束的良类型程序。
对有异常的语言,终态可能还包括规定的异常结果;对带引用的语言,状态需要包含存储,并维护存储类型;对并发语言,进展也可能要区分局部可执行性、死锁和调度假设。每一次扩展,都应明确“值”“一步”和“卡住”究竟指什么。
渐进类型的 cast 演算把检查失败的 blame 作为受控结果。例如未知值被要求用作整数时,布尔标签应触发 blame,而不是把布尔值交给整数原语后卡住。相应的类型安全允许这种规定的失败结果,不保证每次动态检查都成功。
同样,排除了语言定义中的类型错误,不会自动保证程序实现了用户需求、没有逻辑错误或满足某项安全策略。类型系统保证的范围,由语法、类型规则与动态语义共同决定。一个更强的类型系统可以表达更多性质,但需要相应更强的证明。
推论与应用
合起来怎样排除运行卡住
记
反复使用保持定理可得
例如
具有 Bool 类型,按以下顺序执行:
保持性使三项都具有 Bool 类型;进展性保证前两项确实有下一步,而最后一个布尔值是合法结束状态。类型安全的证明不是对这一个轨迹逐项检查,而是保证所有满足前提的轨迹都具有相同结构。
从论文证明到形式化与机器实现
形式化这组定理时,变量绑定、上下文扩展、替换和规范形往往比主定理本身更占工作量。Software Foundations 给出了可核对的机器证明组织:先建立辅助引理,再组合进展、保持与安全性结论。[1]
若把源语言编译到SECD或其他抽象机,还需证明编译与机器执行保留源语言语义,或直接建立机器状态的类型不变量。源语言已经有进展与保持,并不自动证明任意闭包布局、栈操作或返回指令实现都正确。
这条证明路线的价值在于定位设计责任:类型规则约束程序形状,动态规则解释怎样计算,辅助引理连接绑定与替换,最终的不变量把全部运行步骤串起来。对新语言逐项完成这些连接,比仅声称“语言是类型安全的”更有内容。
函数cast核心在规范形中加入函数包装与ground注入,应用时反向检查参数、正向检查结果,并将blame作为受控终态。该页另对一阶立即调用片段证明注解擦除的渐进保证,明确区分单程序安全性与两程序行为比较。有限FIFO会话则给出另一种保持对象:局部剩余协议和两个队列必须共同推进。
参考资料
[1] Benjamin C. Pierce et al., Software Foundations, Volume 2: Programming Language Foundations, Properties of STLC,规范形、进展、替换与保持的开放形式化教材。
[2] Andrew K. Wright and Matthias Felleisen, “A Syntactic Approach to Type Soundness,” Information and Computation 115(1), 1994, pp. 38–94,语法化类型健全性证明的经典工作。
[3] Benjamin C. Pierce, Types and Programming Languages, MIT Press, 2002,Chapters 8–9,类型安全与简单类型 λ 演算。