高级 #compiler#type-system

类型检查与类型推导

类型检查(Type Checking)确保程序中的每个表达式类型一致——整数不能当函数用,字符串不能做除法。类型推导(Type Inference)则能自动推断类型,你不用写 int x = 1 中的 int

类型检查——编译器做”数学题”

int a = 42;
float b = 3.14;
char c = 'x';

a = b;     // 类型不匹配:float → int(隐式转换,可能精度丢失)
c = a + b; // int + float → float,然后 float → char(有损)

类型检查器通过遍历 AST,对每个节点计算类型,并检查一致性。

🏫 类比:拼图的形状 每块拼图有特定的形状(类型)——只有形状匹配的拼图才能拼在一起。类型检查器就是检查拼图形状的工具。

类型系统的分类

类型系统特点例子
静态类型编译时检查类型C, Java, Rust, TypeScript
动态类型运行时检查类型Python, JavaScript, Ruby
强类型类型严格,不隐式转换Rust, Haskell
弱类型允许隐式类型转换C, JavaScript
显式类型必须写类型标注C, Java
类型推导编译器自动推断类型Rust, Haskell, TypeScript

静态 vs 动态——各自的代价

静态类型:编译时发现类型错误 ✓
         写代码时要标注类型(或复杂推导)

动态类型:写代码快,不需要标注类型 ✓
         运行时才暴露类型错误 ✗

💡 渐进类型(Gradual Typing)——TypeScript 在 JavaScript 上加类型标注、Python 的 type hints——在动态语言中引入静态检查。

类型推导——Hindley-Milner 算法

类型推导让编译器自动推断表达式的类型——你不需要写 int x = 42 中的 int

let x = 42;      // 编译器推导出 x: int
let y = x + 1;   // 编译器推导出 y: int
let f a = a + 1; // 编译器推导出 f: int → int

推导过程

表达式:λf. λx. f x(函数应用)

推导步骤:
1. f 有类型 α → β(某个参数类型到某返回值类型)
2. x 有类型 α(因为 x 作为 f 的参数)
3. f x 有类型 β(函数应用的结果类型)
4. 整个表达式的类型:(α → β) → α → β

类型合一(Type Unification)

类型推导的核心算法——合一——解类型方程:

表达式:twice(f, x) = f(f(x))

约束:
1. f 的类型:α → β
2. x 的类型:α
3. f(x) 的类型:β
4. f(f(x)) 的类型:β 且 β = α(因为 f 的参数类型是 α,返回类型是 β,
   而 f(x) 又作为 f 的参数,所以 β = α)
5. 所以 twice 的类型:(α → α) → α → α

运行时类型 vs 编译时类型

// C 语言的类型检查在编译时
// 运行时没有类型信息
struct Animal { void (*speak)(); };
struct Dog { Animal base; int fur_type; };

void treat(Animal* a) {
    a->speak();  // 编译时只知道是 Animal
                 // 运行时可以是 Dog 或 Cat
}

动态分发(Dynamic Dispatch):在运行时根据实际类型决定调用哪个函数——面向对象语言的多态基础。

小结

概念要点
类型检查在 AST 上检查类型一致性
静态 vs 动态编译时 vs 运行时检查
类型推导自动推断表达式类型
Hindley-Milner经典的函数式语言类型推导算法
合一通过解类型方程推导类型

为什么先学这个? 类型检查确保程序语义正确。完成语义分析后,编译器开始生成中间表示(IR)——平台无关的代码表示。