从零编写现代编译器 06 - 类型检查与 Typed HIR
第 05 篇完成了名称解析:每个变量引用都绑定到了确切的定义。但编译器仍然不知道 x + y 是在做整数加法还是布尔运算。这篇给 AST 中的每个表达式节点分配类型,产出一棵全新的中间表示——Typed HIR。类型检查不通过的程序将被拦在代码生成之前。
类型的表示
Sprout 当前只有三种值类型和一个内部哨兵:
1 | |
I64 对应 64 位有符号整数,Bool 对应布尔值,Unit 是没有有意义返回值时使用的类型。Error 不是语言中可写的类型,它只在类型检查发现错误时注入节点,防止一处错误沿表达式树向上引发大量连锁报告。如果操作数之一已经是 Error,类型检查器直接把结果也标为 Error,不再重复报错。
自底向上的类型检查
类型检查器遍历名称已解析的 AST,对每个表达式节点计算类型。过程是自底向上的:先确定叶子节点的类型,再根据操作符的规则推导父节点。
字面量。 整数字面量的类型是 I64,true 和 false 的类型是 Bool。
二元运算。 算术运算符 +、-、*、/ 要求左右操作数都是 I64,结果也是 I64。比较运算符 <、<=、>、>=、==、!= 要求操作数类型相同,结果是 Bool。逻辑运算符 &&、|| 要求操作数都是 Bool,结果也是 Bool。
一元运算。 取负 - 要求操作数为 I64,逻辑非 ! 要求操作数为 Bool。
变量引用。 第 05 篇为每个定义分配了 DefId。类型检查器从符号表中按 DefId 查到声明类型。
赋值。 右侧表达式的类型必须与声明类型一致。至于目标变量是否声明为 mut,第 05 篇的名称解析阶段已经完成了检查——可变性是名字绑定的属性,不涉及类型推导,类型检查器不再重复验证。
return 语句。 返回值的类型必须与当前函数签名中声明的返回类型匹配。
什么是 Typed HIR
类型检查器的输入是 AST,输出是 Typed HIR(高层中间表示)。这不是在原来的 AST 节点上贴一个类型字段那么简单——两者是不同的数据结构。
AST 贴近语法:它保留括号、保留语法糖、保留源码中书写的一切冗余结构。HIR 贴近语义:括号已经融入树结构的嵌套关系,类型已解析到每个节点,后续可能的语法糖也在这里脱去。后续的解释器、控制流构建、SSA 构造都只接受 Typed HIR 作为输入,不再回头查看 AST。
一个 Typed HIR 节点大致长这样:
1 | |
kind 描述这是加法、变量引用还是函数调用;ty 是类型检查器计算出的类型;span 记录对应的源码范围,供后续诊断和源码映射使用。
下面是类型检查器的核心遍历逻辑。它递归地走过每个表达式节点,自底向上推导类型:
1 | |
每个分支的规则对应前面散文描述的类型规则。check_binary_op 和 check_unary_op 按运算符类别返回结果类型,类型不匹配时返回 Ty::Error 并记录诊断。
诊断码与错误门控
每种错误有独立的诊断码,便于文档引用和工具过滤:
| 诊断码 | 含义 | 示例 |
|---|---|---|
| E0201 | 类型不匹配 | let x: i64 = true; |
| E0202 | 未定义变量 | return y;(第 05 篇已实现) |
| E0203 | 对不可变变量赋值 | x = 1;(x 未声明 mut,第 05 篇已实现) |
类型检查器不在遇到第一个错误时中断。它收集所有错误,最后一起报告。这样编程者一次编译就能看到多处问题,不必反复修改、编译、再修改。
1 | |
错误门控是一条关键规则:只要存在任何一个诊断错误,编译器就拒绝进入代码生成阶段。没有通过类型检查的程序不会产出机器码。这比"生成了错误的机器码然后在运行时崩溃"要好得多。Error 类型在这里也起到作用——它让类型检查器能跑完整棵树,收集尽量多的真实错误,同时不因一处错误把后续节点全标红。
两个具体例子
整数赋给布尔变量:
1 | |
类型检查器先判定 true 的类型为 Bool,再与声明类型 I64 比较,发现不匹配,生成 E001。
条件表达式要求布尔值:
1 | |
42 的类型是 I64,但 if 的条件位置要求 Bool,生成 E001。Sprout 不做隐式的整数到布尔转换。
练习
考虑这段程序:
1 | |
如果 if-else 是表达式而不是语句,它的类型应该是什么?两个分支都返回 I64,所以整个 if 表达式的类型是 I64,与 x 的声明一致。如果两个分支类型不同呢?Sprout 目前不支持表达式形式的 if,但这个问题值得在纸上推演一遍:它直接引出"分支类型必须一致"这条规则,和许多语言中 if-else 表达式的设计一致。
资料
Rust 编译器中 HIR 的定义和用途见 rustc dev guide 的 HIR 章节。类型检查的经典参考是 Benjamin Pierce 的 Types and Programming Languages 第 8–11 章,覆盖了简单类型 lambda 演算的类型规则和类型安全证明。
上一篇:05 - 名字、作用域与可变变量。下一篇:07 - 用参考解释器固定语言语义。
