.
类型安全:典范形式、进展与保型
. 类型安全:典范形式、进展与保型
通过典范形式引理、进展定理与保型定理,证明算术语言的良类型词项求值不会受阻.
1 先把命题写下来
“良类型的程序不会出错”这句话, Robin Milner 在 1978 年说成 well-typed programs cannot go wrong. 要把它写成精确命题,必须先规定“出错”是什么: 本文只指规约到受阻状态,即非值且没有后继的词项 ( 正规形式与受阻 ), 不涉及异常、资源耗尽或业务逻辑错误.
要证明的类型安全性表达了类型规则与执行规则之间的协调. 文献常把这类保证称为类型健全性;本文在这一“不受阻”的意义下使用它 (注意与检查器相对类型规则的“算法可靠性”区分).
注意这句话里的量词:对所有良类型的 ,对所有它能多步规约到的 . 这正是 量词的范围 提醒过的地方; 写成符号以后,范围就不会有歧义.
直接证明这个命题不好下手,因为 可以规约任意多步. Andrew K. Wright 与 Matthias Felleisen 在 1994 年给出的办法,是把它拆成两条只涉及一步的定理: 进展说良类型的项要么是值、要么还能一步规约,保证“当前不受阻”; 保型说良类型的项一步规约后得到的项类型不变,保证“规约一步以后,进展还能接着用”.
两条缺一不可. 规约到受阻 里的 现在能规约, 一步规约之后就受阻了. 只有进展,挡不住这种情况;只有保型,挡不住一开始就受阻的项 (这个例子本身没有类型,只是借来说明两条定理各自守住的防线).
2 典范形式
证明进展时需要知道:某个类型的值长什么样.
这条引理把静态信息和动态信息连了起来. 它的前提 是类型信息,类型检查器不运行程序就能在编译期得出; 它的结论“ 是 或 ”是形状信息, 说的是这个值具体长什么样,而求值规则只认形状. 进展的证明正是靠它,从“条件是 类型的值”推出“条件确实是 或 ”, 才能选择 E-IfTrue 或 E-IfFalse. 反过来,若随意加一条定型规则 ,却不改求值规则, 就能通过类型检查; 但没有任何求值规则能处理它,所以会受阻: 它的类型是布尔值,形状却与已有求值规则要求的 或 不符. 动态语言在运行时做的种类检查,正是在实际运行到该位置时才检查这类形状条件.
3 进展
进展回答的是“类型检查通过以后,下一步会不会无规则可用”. 没有它,检查器即使给出了类型,也未必挡住 这样的错误. 具体地,若把 T-Succ 的前提错误放宽成 , 这个项就会得到类型 ,却既非值又不能规约. 进展还必须允许“已经是值”这个分支,否则 这种合法结果反而会判为失败. 对于有异常的真实语言,则要先明确哪些异常是许可的结果, 不能把本文的进展原封不动当作“永不抛异常”.
对照 受阻的原因与上面的证明: 要让 不受阻,要么 能规约,要么 是数值,两者都不成立. 证明之所以没遇到这个麻烦,是因为 T-Succ 要求 , 再经由典范形式把 限定为数值. 类型系统正是在这里把坏程序挡在门外的.
4 保型
进展只能保证当前状态能运行,保型则让这个保证在运行后仍然可用. 若修改 E-PredZero 为 , 原有类型规则仍会给 赋予类型 ,第一步也可以执行; 但它会变成没有类型且受阻的 . 所以一次正确的类型检查必须能跨越每次运行时改写,不能“第一步安全就算安全”. 编译器的优化、解释器的执行规则也要维持相应的不变量, 否则即使前端检查器正确,后端仍能制造坏状态.
5 合起来
下面的推论把“当前不受阻”和“下一项仍有类型”接成对任意有限运行前缀的保证. 只证明进展、缺少保型,保证可能一步后失效; 只证明保型、缺少进展,则起初就受阻的良类型项没有任何一步可走, 保型的条件从未成立,不能排除它. 这个推论保证的是本文定义的“不受阻”, 不保证程序一定终止,也不保证业务逻辑正确, 例如得到 是否符合程序员的意图.
类型安全本身不保证终止,但 规约使大小严格下降 已经另外证明了本文每条规约链都有限. 把它与进展、保型和确定性接起来,还能得到更完整的求值结论: 每个良类型项都会到达唯一的同类型值.
6 练习:类型安全
Exercises (4)
Exercise 5.1. 把进展与保型放到同一条链上
对 写出规约链与每站类型. 逐步说明 进展 在哪里提供后继, 保型 又怎样重建类型, 并解释第一步内部用到的类型为何不都是整项的 .Hint
每个中间项都为 ;条件内部先保持 ,外层 保持 , 不能把这两个类型混成一个固定的 .Solution
四步依次用 E-If + E-IsZero + E-PredSucc,E-If + E-IsZeroZero, E-IfTrue,E-PredZero. 每个非末项都展示了进展要求的一个后继,末项 展示了“已经是值”的分支.
第一步内层 从 保持为 , T-IsZero 重建条件的 ,T-If 再和两个 分支重建整项的 . 第二步条件变成 ,分支不变; 第三步选出的 本来就在 T-If 的前提里具有 ; 第四步由 T-Zero 得 . 这样逐步重建推导,才交代清楚了类型保持的原因.
Exercise 5.2. 扩展:进展与保型的配合
分别考虑两种修改:删除 E-PredZero,或者把它改成 , 其余求值与类型规则都保留. 指出每种修改破坏进展还是保型、另一条是否仍成立, 并用具体项说明只保留其中一条会漏掉什么.Hint
分别修改,不要把两种修改混到同一门语言里. 保型只对实际存在的规约步骤作保证.Solution
删除 E-PredZero 而保留其他规则时, 既非值也没有后继,进展失效. 但剩下的每一步仍是原语言合法的步骤,因此仍保型. 保型无法排除一个根本没有步骤的坏终点.
将 E-PredZero 改为 时, 走到 ,保型失效. 原来的进展证明仍能为每个良类型非值找到一步, T-Pred 的零参数情形只是把后继换了一个;但它不保证后继仍良类型.
例如 在第二种修改下走到受阻的 . 所以“现在有下一步”与“类型保证能传到下一项”必须一起使用, 见 类型安全 .
Exercise 5.3. 保型的逆命题
判断命题“若 且 ,则 ”是否与 保型 等价; 若不等价,给出原语言中的一步反例,并说明它与保型并无矛盾.Hint
从“分支类型不同,但运行选中一个正常分支”的项开始寻找.Solution
不成立. 取 , E-IfTrue 给出 ,且 ,但 没有类型: T-If 要求两个分支具有同一类型,而 与 的类型不同.
这不违反保型,因为保型的前提是起点已有类型. 从一个正常的终点倒推起点有类型,相当于把单向蕴涵改成了逆命题, 类型系统是保守的 已经提醒过不能这样用.
Exercise 5.4. 扩展:不受阻,仍然可以永远运行
只将 E-PredZero 改成 ,其余规则不变. 分析从 出发的运行是否受阻、是否保型、是否终止; 指出进展与保型的证明只需调整哪个情形, 以及“每步让项变小”的论证在哪一步失效.Hint
把 E-PredZero 改成自循环步骤,但不保留它原来规约到 的版本. 检查下一步是否存在、类型是否改变、大小是否下降.Solution
新规则是 . 仍非值,始终有下一步,所以没有受阻; 每一步又回到同一个具有 的项,因此这条链一直保持类型,却永不结束.
原进展证明的 T-Pred 零参数情形仍有后继; 原保型证明的 E-PredZero 情形改为“结果就是起点,已有 ”. 其他情形不变,所以这门修改后的语言仍可证明进展、保型与不受阻的类型安全.
但大小始终为 ,原语言“每一步严格变小”的论证不能再用, 求值循环也不能再保证停止. 类型安全没有承诺终止;要证明终止,还需要另外的下降度量或其他证明.