双向类型检查(bidirectional type checking)是一种简洁有效的类型系统实现方式,将类型推断(infer)和检查(check)分离处理,避免了传统Hindley-Milner算法的统一复杂性。Jimmy Miller在其文章中展示了这一方法的简易性,仅百行代码即可实现小型语言的类型检查器。[1] 本文聚焦let多态λ演算(let-poly),扩展其核心规则至函数抽象(λ)、应用(app)、let绑定及原语操作(primitive ops,如number、string),采用递归下降解析构建抽象语法树(AST),并以基于替换(substitution)的推理引擎驱动规则匹配,实现高效类型验证。
语法定义与AST结构
考虑简化λ演算语法,支持let多态:
Type ::= Num | Str | Type → Type | ∀α. Type (多态量化)
Expr ::= Num(n) | Str(s) | Var(x)
| Lam(x: Type, body: Expr) (注解函数)
| App(fn: Expr, arg: Expr)
| Let(x: Expr, t: Type?, body: Expr) (可选注解let)
AST节点使用变体(variant)表示,例如TypeScript风格:
type Type = {kind: 'Num'} | {kind: 'Str'} | {kind: 'Arr', from: Type, to: Type} | {kind: 'Forall', var: string, body: Type};
type Expr = ;
递归下降解析器(recursive descent parser)直观匹配此语法:每个非终结符对应函数,按优先级(app最低,lam最高)处理。解析let时,先infer value类型,若无注解则泛化(generalize)为∀,存入上下文。
双向类型规则
核心规则基于上下文Γ(Map<string, Type>),infer返回Type,check验证预期类型。
Infer规则(推断模式)
- Num/Str: 返回{kind: 'Num'}/{kind: 'Str'}。
- Var(x): 查Γ,若∀α.τ则实例化(instantiate)为新鲜单态τ'。
- App(fn, arg): infer fn得σ,若σ=α→β则check arg:α,返回β;否则错误。
- Lam(x:τ, body): 抛错(需check模式,提供预期函数类型)。
- Let(x=e,t?,body): infer e得σ;若t则check e:t并σ=t;泛化σ为∀αs.σ'(自由变量αs收集),Γ' = Γ + (x:∀αs.σ');infer Γ' body。
泛化(generalize):收集fv(σ)中Γ外类型变量,形成∀α1..αn.σ。
实例化(instantiate):∀α.τ → τ[新鲜β/α],递归替换。
Check规则(检查模式)
- 默认: infer实际类型,比较相等(typesEqual,递归结构匹配)。
- Lam(x:τ,body): 若预期σ=α→β,check Γ+(x:α) body:β。
- Let: 同infer,但优先用注解。
替换(substitution)引擎:typesubst(τ, {α:β})递归替换自由变量,避免捕获(fresh vars)。
引用Miller:"If you have a type annotation, you can infer the type of the value and check it against that annotation。"[1]
实现引擎要点
上下文管理
- Γ: Map<string, Scheme>(Scheme={vars: string[], type: Type})。
- 进入let/lam:new Map(Γ),推入绑定。
- 退出:无需pop(immutable拷贝)。
替换与实例化
function instantiate(scheme: Scheme): Type {
const fresh = freshVars(scheme.vars.length);
return subst(scheme.type, zip(scheme.vars, fresh));
}
function subst(t: Type, sub: Map<string, Type>): Type {
}
相等性
function typesEqual(a: Type, b: Type): boolean {
}
递归下降解析参数
- Token流:lexer分离标识/符号/字面。
- parseExpr(): peek token,优先lam>app>atom。
- 阈值:深度限10防栈溢;错误恢复:skip至;或}。
可落地参数与清单
- 上下文大小阈值:|Γ|>100抛"ContextOverflow",防无限let。
- 实例化新鲜度:uniqId++生成β,避免冲突;缓存10个预生成。
- 泛化深度:fv(σ)递归限5层,超则单态化。
- 错误优先级:UnboundVar > Mismatch > NonFnCall。
- 监控点:
| 指标 |
阈值 |
动作 |
| infer调用/expr |
>50 |
警告"DeepInference" |
| 替换深度 |
>20 |
回滚单态 |
| 解析错误率 |
>5% |
语法高亮提示 |
回滚策略:infer失败→尝试check默认模式;let无注解→fallback monotype。
示例验证
考虑let id = λx.x in id 42:
- infer let id=(λx.x): infer lam需预期→Num→Num,check body x:Num。
- 泛化id: ∀α.α→α。
- body id 42: instantiate ∀→Num→Num,app check。
另一个:let f = λx:Num.x+1 in f "a" → Mismatch Str/Num。
此框架扩展性强:加primitive ops如+(check Num→Num→Num);支持block用嵌套let。
性能:单expr<1ms(TS基准);生产阈值:1s超时。
来源:
[1] Jimmy Miller, "The Easiest Way to Build a Type Checker", https://jimmyhmiller.com/easiest-way-to-build-type-checker (2023访问)。
[2] TAPL Ch.13-14, Simply Typed Lambda + Let Poly (Pierce参考)。
(正文字数:1028)