7.2 类型检查与转换:让每个操作满足语言规则
同一个乘号在不同语言里可能代表数值乘法、字符串重复,也可能非法;语义检查室必须遵守当前语言的规则。
"hello" * 3 在某些语言中合法,表示重复字符串;在另一些语言中是类型错误。语义分析不能靠一张“普遍操作符表”判断,它实现的是某门语言明确规定的类型规则。
本课目标
- 区分类型综合、类型检查和类型推断;
- 为运算符、调用与返回建立约束;
- 区分隐式转换、显式转换与表示转换;
- 使用 error type 抑制级联错误。
1. 类型环境与判断式
类型规则常写成判断:
$$ \Gamma\vdash e:T, $$
表示在环境 $\Gamma$ 中,表达式 $e$ 具有类型 $T$。
例如整数加法:
$$ \frac{\Gamma\vdash e_1:Int\qquad\Gamma\vdash e_2:Int} {\Gamma\vdash e_1+e_2:Int}. $$
环境不只是名字到类型的映射,还可能包含泛型参数、生命周期、可变性、效果和当前函数返回类型。
2. 综合与检查是两个方向
- 综合
synth(expr) -> Type:从表达式自身推导类型; - 检查
check(expr, expected):给定期望类型,验证表达式是否满足。
字面量 42 可综合为默认整数类型,也可在期望 u8 的位置直接按 u8 检查并做范围验证。Lambda 参数若没有标注,常需要调用上下文提供期望函数类型。
双向类型检查在“局部推断”和“可预测错误”之间取得平衡,不要求所有节点都无上下文地推导。
3. 二元运算符需要解析规则
对 left + right:
- 获取或检查左右类型;
- 查找内建规则或重载候选;
- 求需要的转换;
- 选择唯一最佳候选;
- 在 AST/HIR 中插入显式 coercion 节点;
- 返回结果类型。
int + int → int
float + float → float
int + float → 是否转 float,由语言规定
string + string → 是否连接,由语言规定
pointer + int → 是否允许及步长语义,由语言规定比较结果也不必固定是 int;许多语言返回 bool。== 是否接受任意同类型值,还取决于该类型是否定义相等性、浮点 NaN 语义和用户重载。
4. 隐式转换必须有方向和成本
转换图可能包含:
- widening:较窄整数到较宽整数;
- numeric promotion:整数到浮点;
- subtype coercion:子类型到父类型;
- dereference、borrow 或 boxing;
- 用户定义转换。
若两个重载都可用,需要按转换成本和特异性排序。排序不应依赖哈希表迭代顺序,否则同一程序可能在不同运行中选择不同候选。
窄化、可能丢精度或改变符号的转换通常要求显式写出。即使语法允许,编译器也可提供可配置警告。
5. 函数调用建立一组约束
调用 f(a,b) 至少检查:
- 被调用对象是否可调用;
- 实参数量与命名参数规则;
- 每个实参能否转换到形参类型;
- 泛型参数能否满足约束;
- 返回类型如何进入外层期望;
- 可变参数与默认参数怎样展开。
重载解析常先收集同名候选,再过滤不可行者,最后选唯一最佳候选。失败诊断应说明最接近候选为什么不匹配,而不是只说“找不到函数”。候选很多时需限制输出并排序。
6. 控制流影响类型与初始化
let x;
if cond { x = 1; }
print(x);仅按 AST 局部递归无法证明 x 在所有路径上初始化。需要控制流图上的 definite assignment 分析。
同理:
- 所有路径是否返回;
- 类型缩窄在分支内是否成立;
- 可空值是否已检查;
- move 后变量是否仍被使用。
这些规则通常与类型系统协作,但实现上可能是独立数据流 pass。
7. Error type 阻止级联
unknown + 1 * true若 unknown 已报告未声明,可令它类型为 ErrorType。运算遇到 ErrorType 时返回 ErrorType,而不再报告“不能与 int 相加”等派生错误。
TypeId check_binary(BinaryExpr *expr) {
TypeId left = check_expr(expr->left);
TypeId right = check_expr(expr->right);
if (is_error(left) || is_error(right)) return TYPE_ERROR;
OperatorResult result = resolve_operator(expr->op, left, right);
if (!result.ok) {
report_operator_mismatch(expr->span, expr->op, left, right);
return TYPE_ERROR;
}
insert_coercions(expr, result);
return result.type;
}ErrorType 应避免进入代码生成;前端有错误时可以停止生成,IDE 模式则保留部分 typed tree 供功能降级。
8. 类型等价也需定义
两种类型何时相等?
- 名义类型:声明身份相同才相等;
- 结构类型:组成结构相同即可;
- 类型别名:透明还是创建新类型;
- 泛型实例:参数相同及构造器相同;
- 递归类型:需要避免无限展开。
把类型打印字符串后比较既慢又不可靠。编译器通常 intern 类型,使用稳定 TypeId,并提供规范化与等价判断。
常见误区
- 类型检查就是比较左右类型是否相同:转换、重载、子类型和期望类型都会参与。
- 隐式转换只是插一条机器指令:它首先是语言语义,某些转换运行时无需指令,另一些可能分配内存。
- 局部递归能完成所有静态检查:路径相关性质需要 CFG 数据流。
- 类型错误后返回任意默认类型即可:会制造级联错误甚至错误代码生成。
练习
- 为一门小语言定义
+的完整类型矩阵,并说明每个隐式转换。 - 设计两个重载,使一个实参同时可转换到二者,制定无歧义排序规则。
- 为未初始化变量构造 CFG,并写出数据流状态。
- 实现 ErrorType 传播测试,确保根因只报告一次。
小结
类型检查是语言规则的可执行版本:它在环境中建立约束、解析重载、插入转换并处理控制流事实。正确性来自明确规则,而不是把某种语言的直觉当作通用真理。
下一课把已经绑定并定型的树降低为 IR,显式表示基本块、控制流和数据依赖。