7.2 类型检查与转换:让每个操作满足语言规则
"hello" * 3 在某些语言中合法,表示重复字符串;在另一些语言中是类型错误。语义分析不能靠一张“普遍操作符表”判断,它实现的是某门语言明确规定的类型规则。
本课目标
- 区分类型综合、类型检查和类型推断;
- 为运算符、调用与返回建立约束;
- 区分隐式转换、显式转换与表示转换;
- 使用 error type 抑制级联错误。
1. 类型环境与判断式
类型规则常写成判断:
表示在环境
例如整数加法:
环境通常以已经解析的符号身份到类型的映射为基础,也可能包含泛型参数、生命周期、可变性、效果和当前函数返回类型。
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;
- 用户定义转换。
若两个重载都可用,需要按语言规定的转换等级、适用阶段和特异性比较;不一定能把它们简化成一个总分。若仍没有唯一最佳候选,就应报告歧义。排序不应依赖哈希表迭代顺序,否则同一程序可能在不同运行中选择不同候选。
窄化或改变符号的转换可能要求显式写出,但“可能丢精度”不等于“一定显式”:有些语言允许整数隐式转浮点,即使大整数不能精确表示。
数值转换与位表示重解释也不同。IEEE 754 binary32 的 1.0 转整数得到 1;若按语言允许的方式把同一组 32 位重解释为无符号整数,得到的是 0x3f800000,即 1065353216。不能把二者都叫成“换个类型名字”。
5. 函数调用建立一组约束
调用 f(a,b) 至少检查:
- 被调用对象是否可调用;
- 实参数量与命名参数规则;
- 每个实参能否转换到形参类型;
- 泛型参数能否满足约束;
- 返回类型如何进入外层期望;
- 可变参数与默认参数怎样展开。
重载解析常先收集同名候选,再过滤不可行者,最后选唯一最佳候选。失败诊断应说明最接近候选为什么不匹配,而不是只说“找不到函数”。候选很多时需限制输出并排序。
6. 控制流影响类型与初始化
let x;
if cond { x = 1; }
print(x);仅比较局部节点类型不足以证明 x 在使用前已经初始化。需要沿控制流传播 definite assignment 事实:可在显式 CFG 上分析,也可在结构化 AST 上递归传播分支状态。对所有可达前驱都必须成立的事实,在汇合点通常取交集。
同理:
- 所有路径是否返回;
- 类型缩窄在分支内是否成立;
- 可空值是否已检查;
- move 后变量是否仍被使用。
这些规则通常与类型系统协作,但实现上可能是独立数据流 pass。
7. Error type 阻止级联
unknown + 1 * true若 unknown 已报告未声明,可令它类型为 ErrorType。运算遇到 ErrorType 时返回 ErrorType,而不再报告由它引出的相加错误。但右侧 1 * true 仍是独立错误,必须先检查它;不能一看到左侧 ErrorType 就跳过整个右子树。下面是接入既有类型检查器的 C 片段,TypeId、AST、解析与诊断接口由宿主提供:
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,并写出数据流状态。
- 实现 ErrorType 传播测试,确保根因只报告一次。
小结
类型检查是语言规则的可执行版本:它在环境中建立约束、解析重载、插入转换并处理控制流事实。正确性来自明确规则,而不是把某种语言的直觉当作通用真理。
下一课把已经绑定并定型的树降低为 IR,显式表示基本块、控制流和数据依赖。