语法分析构建了 AST,但 AST 只记录了程序的结构——它不知道 x + y 中的 x 和 y 是什么类型,不知道 x 是否已声明,不知道 int 和 float 能否相加。这些语义信息需要语义分析来填充。
语义分析是编译器前端的最后一个阶段,也是连接前端和后端的桥梁——它将”语法正确但语义未知”的 AST 转换为”语义完整”的带类型 AST,为后续的 IR 生成和优化提供必要信息。
一、语义分析的任务与定位
1.1 语义分析在编译流水线中的位置
语义分析的核心任务:
- 类型检查:验证操作的类型合法性(
int + float合法,int + string非法) - 名称解析:将标识符绑定到其声明(变量
x指向哪个声明?) - 作用域管理:处理变量的可见性规则(内层作用域可以遮蔽外层)
- 类型推导:推断未显式标注的表达式类型
- 语义规则验证:检查 break 只在循环内、return 类型匹配等
1.2 一个具体的例子
int foo(int x) { float y = 3.14; return x + y; // int + float → 隐式转换为 float,但返回 int → 类型错误?}语义分析需要回答:
| 问题 | 答案 |
|---|---|
x 的类型是什么? | int(参数声明) |
y 的类型是什么? | float(变量声明) |
x + y 合法吗? | 合法,int 隐式转换为 float |
x + y 的结果类型? | float |
return x + y 合法吗? | 取决于语言:C 会截断为 int,Rust 会报错 |
二、类型系统
2.1 类型系统的作用
类型系统是编程语言的安全网——它在编译期或运行时阻止不合法的操作:
2.2 类型系统的分类
| 分类维度 | 类别 A | 类别 B | 示例 |
|---|---|---|---|
| 检查时机 | 静态类型(编译期) | 动态类型(运行时) | C/Rust vs Python/JS |
| 类型强度 | 强类型(不允许隐式转换) | 弱类型(允许隐式转换) | Python/Rust vs C/JS |
| 类型声明 | 显式类型 | 类型推导 | Java/C vs ML/Haskell/Rust |
| 多态性 | 参数多态 | 子类型多态 | Rust trait vs Java 继承 |
2.3 类型检查规则
类型检查的核心是类型规则——每条规则描述一个操作对操作数类型的要求和结果的类型:
# 类型规则的表示# Γ ⊢ e1 : T1 Γ ⊢ e2 : T2 T1 和 T2 可相加# ────────────────────────────────────────────────────# Γ ⊢ e1 + e2 : T_result
class TypeChecker: """类型检查器"""
# 算术运算的类型规则 ARITH_RULES = { ('int', 'int'): 'int', ('int', 'float'): 'float', ('float', 'int'): 'float', ('float', 'float'): 'float', }
# 比较运算的结果类型 CMP_RESULT = 'bool'
def check_binary_op(self, op: str, left_type: str, right_type: str) -> str: """检查二元运算的类型合法性""" if op in ('+', '-', '*', '/'): key = (left_type, right_type) if key not in self.ARITH_RULES: raise TypeError( f"Cannot apply '{op}' to {left_type} and {right_type}" ) return self.ARITH_RULES[key]
elif op in ('<', '>', '==', '!=', '<=', '>='): if left_type != right_type: raise TypeError( f"Cannot compare {left_type} and {right_type}" ) return self.CMP_RESULT
else: raise TypeError(f"Unknown operator: {op}")2.4 隐式类型转换
当操作数类型不匹配但可以转换时,编译器会插入隐式类型转换:
| 语言 | 隐式转换策略 | 示例 |
|---|---|---|
| C | 宽化转换自动,窄化转换警告 | int + float → float,float → int 警告 |
| Java | 宽化自动,窄化需显式 | int + double → double,double → int 需 cast |
| Rust | 几乎不允许隐式转换 | i32 + f64 报错,需显式 as f64 |
| Go | 不允许隐式转换 | int32 + int64 报错,需显式转换 |
| Python | 动态类型,运行时转换 | 1 + 1.5 → 2.5 |
Rust 和 Go 的严格类型转换是设计选择——它们认为隐式转换是 bug 的常见来源。C 的宽松转换则是历史遗留——早期 C 程序员需要灵活地操作底层类型。
三、符号表与作用域
3.1 符号表的作用
符号表是编译器的”记忆”——它记录每个标识符的声明信息:
@dataclassclass Symbol: name: str # 符号名称 type: str # 类型 kind: str # 种类:variable, function, parameter, type scope: int # 所在作用域层级 offset: int # 栈帧偏移(用于代码生成) is_initialized: bool # 是否已初始化 line: int # 声明所在行号
class SymbolTable: """符号表"""
def __init__(self): self.scopes = [{}] # 作用域栈,全局作用域在最底层 self.scope_counter = 0
def enter_scope(self): """进入新作用域""" self.scope_counter += 1 self.scopes.append({})
def exit_scope(self): """退出作用域""" self.scopes.pop()
def define(self, symbol: Symbol): """在当前作用域定义符号""" current_scope = self.scopes[-1] if symbol.name in current_scope: raise SemanticError( f"Redefinition of '{symbol.name}' at line {symbol.line}" ) current_scope[symbol.name] = symbol
def lookup(self, name: str) -> Symbol: """查找符号,从内层作用域向外层搜索""" for scope in reversed(self.scopes): if name in scope: return scope[name] raise SemanticError(f"Undefined symbol: '{name}'")
def lookup_current_scope(self, name: str) -> Symbol: """只在当前作用域查找""" if name in self.scopes[-1]: return self.scopes[-1][name] return None3.2 作用域规则
3.3 不同语言的作用域规则
| 语言 | 作用域类型 | 特殊规则 |
|---|---|---|
| C | 块作用域、文件作用域 | static 限制为文件作用域 |
| Java | 类作用域、方法作用域、块作用域 | 内部类可访问外部类成员 |
| Python | 函数作用域(LEGB 规则) | global/nonlocal 关键字 |
| Rust | 块作用域、模块作用域 | 所有权转移后不可访问 |
| Go | 块作用域、包作用域 | 短变量声明 := 限制 |
3.4 符号表实现对比
| 实现方式 | 查找复杂度 | 插入复杂度 | 适用场景 |
|---|---|---|---|
| 哈希表 | O(1) | O(1) | 最常用,大多数编译器 |
| 红黑树 | O(log n) | O(log n) | 需要有序遍历 |
| 链表 | O(n) | O(1) | 符号数量少 |
| Trie | O(k) | O(k) | 需要前缀匹配 |
四、类型推导
4.1 Hindley-Milner 类型推导
Hindley-Milner(HM)类型推导是函数式语言的基石——它可以在不需要显式类型标注的情况下推导出表达式的类型:
# HM 类型推导的核心算法:Algorithm W
class TypeVar: """类型变量""" def __init__(self, name): self.name = name self.instance = None # 绑定的具体类型
class TypeOperator: """类型构造器""" def __init__(self, name, args): self.name = name self.args = args # 参数类型列表
# 基本类型INT_TYPE = TypeOperator('int', [])FLOAT_TYPE = TypeOperator('float', [])BOOL_TYPE = TypeOperator('bool', [])
# 函数类型:a -> bdef function_type(arg_type, ret_type): return TypeOperator('->', [arg_type, ret_type])
def unify(t1, t2, subst): """合一算法:找到使 t1 = t2 的替换""" t1 = prune(t1, subst) t2 = prune(t2, subst)
if isinstance(t1, TypeVar): if t1 != t2: if occurs_in(t1, t2, subst): raise TypeError("Recursive type") subst[t1.name] = t2 elif isinstance(t2, TypeVar): unify(t2, t1, subst) elif isinstance(t1, TypeOperator) and isinstance(t2, TypeOperator): if t1.name != t2.name or len(t1.args) != len(t2.args): raise TypeError(f"Cannot unify {t1.name} with {t2.name}") for a1, a2 in zip(t1.args, t2.args): unify(a1, a2, subst) else: raise TypeError(f"Cannot unify types")
def prune(t, subst): """跟随替换链""" if isinstance(t, TypeVar) and t.name in subst: return prune(subst[t.name], subst) return t4.2 类型推导示例
# 推导 let f = fun x -> x + 1 的类型# 1. x : α (新类型变量)# 2. 1 : int# 3. x + 1 : 需要 x : int,所以 α = int# 4. fun x -> x + 1 : int -> int# 5. f : int -> int
# 推导 let id = fun x -> x 的类型# 1. x : α (新类型变量)# 2. x : α# 3. fun x -> x : α -> α# 4. id : ∀α. α -> α (多态类型)4.3 类型推导策略对比
| 策略 | 语言 | 特点 |
|---|---|---|
| HM 推导 | ML, Haskell | 完整推导,多态 |
| 局部推导 | Rust, Swift | 需要函数签名,推导局部变量 |
| 流敏感推导 | C# var, C++ auto | 从初始化表达式推导 |
| 无推导 | C, Go | 所有类型显式标注 |
五、完整的语义分析器
5.1 语义分析器的实现
class SemanticAnalyzer(ASTVisitor): """语义分析器"""
def __init__(self): self.symbol_table = SymbolTable() self.current_function_return_type = None self.errors = [] self.loop_depth = 0 # 跟踪嵌套循环深度
def error(self, msg, node): self.errors.append(SemanticError(msg, node))
def visit_VarDecl(self, node): # 检查类型是否有效 if not self.is_valid_type(node.type_name): self.error(f"Unknown type: {node.type_name}", node) return
# 检查初始化表达式的类型 if node.init: init_type = self.visit(node.init) if init_type and not self.is_assignable(node.type_name, init_type): self.error( f"Cannot assign {init_type} to {node.type_name}", node )
# 在符号表中定义变量 symbol = Symbol( name=node.name, type=node.type_name, kind='variable', scope=self.symbol_table.scope_counter, offset=0, is_initialized=node.init is not None, line=0 ) self.symbol_table.define(symbol)
def visit_BinaryOp(self, node): left_type = self.visit(node.left) right_type = self.visit(node.right)
if left_type and right_type: try: return self.check_binary_op(node.op, left_type, right_type) except TypeError as e: self.error(str(e), node)
return None
def visit_Identifier(self, node): try: symbol = self.symbol_table.lookup(node.name) return symbol.type except SemanticError as e: self.error(str(e), node) return None
def visit_FuncDecl(self, node): # 在外层作用域定义函数 func_symbol = Symbol( name=node.name, type=node.return_type, kind='function', scope=self.symbol_table.scope_counter, offset=0, is_initialized=True, line=0 ) self.symbol_table.define(func_symbol)
# 进入函数作用域 self.symbol_table.enter_scope() self.current_function_return_type = node.return_type
# 定义参数 for param in node.params: param_symbol = Symbol( name=param.name, type=param.type_name, kind='parameter', scope=self.symbol_table.scope_counter, offset=0, is_initialized=True, line=0 ) self.symbol_table.define(param_symbol)
# 分析函数体 self.visit(node.body)
# 退出函数作用域 self.symbol_table.exit_scope() self.current_function_return_type = None
def visit_ReturnStmt(self, node): if self.current_function_return_type is None: self.error("Return outside function", node) return
if node.value: return_type = self.visit(node.value) if return_type and not self.is_assignable( self.current_function_return_type, return_type ): self.error( f"Cannot return {return_type} from function " f"with return type {self.current_function_return_type}", node )
def is_assignable(self, target_type, source_type): """检查是否可以赋值""" if target_type == source_type: return True # 隐式转换规则 implicit_conversions = { ('float', 'int'): True, # int → float ('long', 'int'): True, # int → long ('double', 'float'): True, # float → double ('double', 'int'): True, # int → double } return implicit_conversions.get((target_type, source_type), False)
def is_valid_type(self, type_name): return type_name in {'int', 'float', 'double', 'long', 'void', 'bool', 'char'}5.2 语义分析的处理流程
六、高级语义分析
6.1 定性赋值分析
class DefiniteAssignmentChecker: """定性赋值分析:确保变量在使用前已被赋值"""
def __init__(self): self.assigned = set() # 已赋值的变量集合
def visit_VarDecl(self, node): if node.init: self.assigned.add(node.name)
def visit_Identifier(self, node): if node.name not in self.assigned: raise SemanticError( f"Variable '{node.name}' used before assignment" )
def visit_IfStmt(self, node): then_assigned = set() else_assigned = set()
# 分析 then 分支 old_assigned = self.assigned.copy() self.visit(node.then_branch) then_assigned = self.assigned - old_assigned
# 分析 else 分支 self.assigned = old_assigned.copy() if node.else_branch: self.visit(node.else_branch) else_assigned = self.assigned - old_assigned
# 两个分支都赋值的变量才认为是已赋值 self.assigned = old_assigned | (then_assigned & else_assigned)6.2 可达性分析
class ReachabilityChecker: """可达性分析:检查死代码和缺少 return 语句"""
def visit_FuncDecl(self, node): reachable = self.visit(node.body) if not reachable and node.return_type != 'void': self.error( f"Function '{node.name}' may not return a value", node )
def visit_ReturnStmt(self, node): return False # return 后的代码不可达
def visit_IfStmt(self, node): then_reachable = self.visit(node.then_branch) else_reachable = ( self.visit(node.else_branch) if node.else_branch else True ) return then_reachable or else_reachable
def visit_Block(self, node): reachable = True for stmt in node.stmts: if not reachable: self.warning("Unreachable code", stmt) reachable = self.visit(stmt) return reachable6.3 语义分析检查项汇总
| 检查项 | 说明 | 示例错误 |
|---|---|---|
| 类型检查 | 操作数类型是否匹配 | int + string |
| 名称解析 | 标识符是否已声明 | 使用未声明变量 |
| 重复定义 | 同一作用域是否重复声明 | int x; int x; |
| 返回类型 | return 表达式类型是否匹配 | int f() { return "hi"; } |
| 定性赋值 | 变量是否在使用前赋值 | int x; return x; |
| 可达性 | 是否有死代码 | return; x = 1; |
| break/continue | 是否在循环内 | 循环外 break |
| 函数调用 | 参数数量和类型是否匹配 | f(1, 2) 但 f 只接受 1 个参数 |
七、不同语言的语义分析对比
| 语言 | 类型检查 | 名称解析 | 特殊语义 |
|---|---|---|---|
| C | 宽松,大量隐式转换 | 单遍(需前向声明) | 未定义行为 |
| C++ | 复杂,模板类型推导 | 两阶段名称查找 | SFINAE、Concepts |
| Rust | 严格,借用检查 | 简单 | 生命周期、trait 约束 |
| Go | 严格,无隐式转换 | 简单 | 接口隐式实现 |
| Java | 中等,子类型多态 | 简单 | 泛型擦除 |
| Python | 运行时类型检查 | 动态 | 鸭子类型 |
C 语言需要前向声明(int foo(int x);)是因为其编译器是单遍的——它在看到函数调用时必须已经知道函数的签名。C++ 和 Java 不需要前向声明,因为它们的编译器会先扫描所有声明再进行类型检查。
八、动手实践
8.1 实验一:构建完整的语义分析器
# 组合词法分析、语法分析和语义分析source = """int add(int a, int b) { return a + b;}
int main() { int x = add(3, 4); return x;}"""
# 词法分析lexer = Lexer(source)tokens = lexer.tokenize()
# 语法分析parser = Parser(tokens)ast = parser.parse_program()
# 语义分析analyzer = SemanticAnalyzer()analyzer.visit(ast)
if analyzer.errors: for error in analyzer.errors: print(f"Error: {error}")else: print("Semantic analysis passed!")8.2 实验二:观察 Rust 的类型推导
# Rust 需要函数签名,但可以推导局部变量cat > type_inference.rs << 'EOF'fn main() { let x = 42; // 推导为 i32 let y = 3.14; // 推导为 f64 let z = x as f64 + y; // z: f64 println!("{}", z);}EOF
rustc type_inference.rs && ./type_inference8.3 实验三:Clang 的语义分析输出
# 观察类型错误cat > type_error.c << 'EOF'int main() { int x = "hello"; // 类型不匹配 return x + 1.5; // 隐式转换警告}EOF
clang -Wall type_error.c 2>&1九、本章小结
在上一章中,语法分析将 Token 流构建为 AST,让编译器看到了程序的结构。但 AST 只记录了语法形状——它不知道 x + y 中 x 是什么类型,也不知道 x 是否已声明。要让程序从”语法正确”变为”语义完整”,就需要语义分析来填充类型信息和验证语义规则。
| 概念 | 要点 |
|---|---|
| 语义分析任务 | 类型检查、名称解析、作用域管理、类型推导、语义规则验证 |
| 类型系统 | 静态 vs 动态、强 vs 弱、显式 vs 推导 |
| 符号表 | 记录标识符的声明信息,支持作用域嵌套 |
| 作用域 | 内层可访问外层,同名遮蔽,不同语言规则不同 |
| 类型推导 | HM 算法、局部推导、流敏感推导 |
| 隐式转换 | C 宽松、Rust/Go 严格,是设计选择 |
| 定性赋值 | 确保变量在使用前已赋值 |
| 可达性 | 检测死代码和缺少 return |
到这里,语义分析的主要机制已经梳理清楚。下一章进入 中间表示,看看带类型的 AST 如何被转换为编译器中端的核心数据结构——IR 与 SSA。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






