跳到内容

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

  1. 获取或检查左右类型;
  2. 查找内建规则或重载候选;
  3. 求需要的转换;
  4. 选择唯一最佳候选;
  5. 在 AST/HIR 中插入显式 coercion 节点;
  6. 返回结果类型。
text
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. 控制流影响类型与初始化

text
let x;
if cond { x = 1; }
print(x);

仅按 AST 局部递归无法证明 x 在所有路径上初始化。需要控制流图上的 definite assignment 分析。

同理:

  • 所有路径是否返回;
  • 类型缩窄在分支内是否成立;
  • 可空值是否已检查;
  • move 后变量是否仍被使用。

这些规则通常与类型系统协作,但实现上可能是独立数据流 pass。

7. Error type 阻止级联

text
unknown + 1 * true

unknown 已报告未声明,可令它类型为 ErrorType。运算遇到 ErrorType 时返回 ErrorType,而不再报告“不能与 int 相加”等派生错误。

c
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 数据流。
  • 类型错误后返回任意默认类型即可:会制造级联错误甚至错误代码生成。

练习

  1. 为一门小语言定义 + 的完整类型矩阵,并说明每个隐式转换。
  2. 设计两个重载,使一个实参同时可转换到二者,制定无歧义排序规则。
  3. 为未初始化变量构造 CFG,并写出数据流状态。
  4. 实现 ErrorType 传播测试,确保根因只报告一次。

小结

类型检查是语言规则的可执行版本:它在环境中建立约束、解析重载、插入转换并处理控制流事实。正确性来自明确规则,而不是把某种语言的直觉当作通用真理。

下一课把已经绑定并定型的树降低为 IR,显式表示基本块、控制流和数据依赖。

Built with VitePress | Software Systems Atlas