mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1932 字
5 分钟
语义分析:类型检查与作用域
2025-12-22

语法分析构建了 AST,但 AST 只记录了程序的结构——它不知道 x + y 中的 xy 是什么类型,不知道 x 是否已声明,不知道 intfloat 能否相加。这些语义信息需要语义分析来填充。

语义分析是编译器前端的最后一个阶段,也是连接前端和后端的桥梁——它将”语法正确但语义未知”的 AST 转换为”语义完整”的带类型 AST,为后续的 IR 生成和优化提供必要信息。

一、语义分析的任务与定位#

1.1 语义分析在编译流水线中的位置#

flowchart LR AST["AST<br/>语法结构"] -->|语义分析| TAST["带类型 AST<br/>语义完整"] TAST -->|IR 生成| IR["中间表示"] style AST fill:#e3f2fd,stroke:#1565c0 style TAST fill:#fff3e0,stroke:#e65100 style IR fill:#e8f5e9,stroke:#2e7d32

语义分析的核心任务:

  1. 类型检查:验证操作的类型合法性(int + float 合法,int + string 非法)
  2. 名称解析:将标识符绑定到其声明(变量 x 指向哪个声明?)
  3. 作用域管理:处理变量的可见性规则(内层作用域可以遮蔽外层)
  4. 类型推导:推断未显式标注的表达式类型
  5. 语义规则验证:检查 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 类型系统的作用#

类型系统是编程语言的安全网——它在编译期或运行时阻止不合法的操作:

graph TB TS["类型系统"] TS --> SAFETY["安全保证"] TS --> OPT["编译优化"] TS --> DOC["文档作用"] TS --> ABSTRACTION["抽象工具"] SAFETY --> S1["防止内存错误"] SAFETY --> S2["防止类型混淆"] SAFETY --> S3["防止空指针"] OPT --> O1["消除运行时检查"] OPT --> O2["选择正确指令"] OPT --> O3["内联优化"] style TS fill:#e3f2fd,stroke:#1565c0 style SAFETY fill:#e8f5e9,stroke:#2e7d32 style OPT fill:#fff3e0,stroke:#e65100

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 隐式类型转换#

当操作数类型不匹配但可以转换时,编译器会插入隐式类型转换

flowchart LR subgraph C语言隐式转换层次 CHAR["char"] --> INT["int"] INT --> LONG["long"] LONG --> FLOAT["float"] FLOAT --> DOUBLE["double"] end style CHAR fill:#e3f2fd,stroke:#1565c0 style DOUBLE fill:#fce4ec,stroke:#c62828
语言隐式转换策略示例
C宽化转换自动,窄化转换警告int + float → floatfloat → int 警告
Java宽化自动,窄化需显式int + double → doubledouble → int 需 cast
Rust几乎不允许隐式转换i32 + f64 报错,需显式 as f64
Go不允许隐式转换int32 + int64 报错,需显式转换
Python动态类型,运行时转换1 + 1.5 → 2.5
Note

Rust 和 Go 的严格类型转换是设计选择——它们认为隐式转换是 bug 的常见来源。C 的宽松转换则是历史遗留——早期 C 程序员需要灵活地操作底层类型。

三、符号表与作用域#

3.1 符号表的作用#

符号表是编译器的”记忆”——它记录每个标识符的声明信息:

@dataclass
class 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 None

3.2 作用域规则#

flowchart TB subgraph 作用域嵌套 G["全局作用域<br/>int global_var"] G --> F["函数作用域 foo<br/>int param"] F --> B1["块作用域 1<br/>int x"] F --> B2["块作用域 2<br/>int x ← 遮蔽外层"] B1 --> N1["嵌套块<br/>int y"] end style G fill:#e3f2fd,stroke:#1565c0 style F fill:#fff3e0,stroke:#e65100 style B1 fill:#e8f5e9,stroke:#2e7d32 style B2 fill:#e8f5e9,stroke:#2e7d32 style N1 fill:#fce4ec,stroke:#c62828

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)符号数量少
TrieO(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 -> b
def 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 t

4.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 语义分析的处理流程#

flowchart TB AST["输入 AST"] --> PASS1["第一遍:收集声明<br/>建立符号表"] PASS1 --> PASS2["第二遍:类型检查<br/>验证语义规则"] PASS2 --> PASS3["第三遍:标注类型<br/>生成带类型 AST"] PASS3 --> TAST["输出带类型 AST"] PASS2 --> ERRORS["收集错误"] style AST fill:#e3f2fd,stroke:#1565c0 style TAST fill:#e8f5e9,stroke:#2e7d32 style ERRORS fill:#fce4ec,stroke:#c62828

六、高级语义分析#

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 reachable

6.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运行时类型检查动态鸭子类型
Tip

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_inference

8.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 + yx 是什么类型,也不知道 x 是否已声明。要让程序从”语法正确”变为”语义完整”,就需要语义分析来填充类型信息和验证语义规则。

概念要点
语义分析任务类型检查、名称解析、作用域管理、类型推导、语义规则验证
类型系统静态 vs 动态、强 vs 弱、显式 vs 推导
符号表记录标识符的声明信息,支持作用域嵌套
作用域内层可访问外层,同名遮蔽,不同语言规则不同
类型推导HM 算法、局部推导、流敏感推导
隐式转换C 宽松、Rust/Go 严格,是设计选择
定性赋值确保变量在使用前已赋值
可达性检测死代码和缺少 return

到这里,语义分析的主要机制已经梳理清楚。下一章进入 中间表示,看看带类型的 AST 如何被转换为编译器中端的核心数据结构——IR 与 SSA。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

语义分析:类型检查与作用域
https://blog.souloss.com/posts/compiler/semantic-analysis/
作者
Souloss
发布于
2025-12-22
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时