前言
在上一篇文章中,我们讲了语法分析如何把代码变成一棵树。但这还不够。
就像这个句子:
“无色的绿色思想愤怒地睡觉”
语法完全正确(形容词+形容词+名词+副词+动词),但毫无意义。
编译器也一样。即使代码语法正确,也可能语义错误:
"hello" + 123 # 语法正确,但字符串能加数字吗?
这就是语义分析要做的事:检查代码是否有意义。
一、语义分析是什么?
1.1 从生活例子说起
想象你是语文老师,批改学生的作文:
语法检查:
- ✅ “小明吃苹果” - 语法正确
- ❌ “苹果吃小明” - 语法错误
语义检查:
- ✅ “小明吃苹果” - 合理
- ❌ “苹果吃小明” - 语法正确,但语义不对(苹果不会吃人)
编译器的语义分析就像第二道关卡:
flowchart LR
A[源代码] --> B[词法分析]
B --> C[语法分析]
C --> D[语义分析]
D --> E[中间代码]
style D fill:#fff4e1
style A fill:#e1f5ff
style E fill:#d4edda
1.2 语义分析要做什么?
主要任务:
- 建立符号表:记录所有变量、函数的信息
- 类型检查:确保类型匹配(整数加整数,不是整数加字符串)
- 作用域检查:变量在哪里可用
- 其他检查:未定义变量、重复定义、死代码等
二、符号表:编译器的记事本
2.1 为什么需要符号表?
想象你在看一部小说,人物很多:
第1章:小明出场
第10章:小红出场
第50章:小明又出现了... 他是谁来着?
你需要一个人物表来记录:
小明 - 主角,学生,第1章出场
小红 - 女主角,第10章出场
符号表就是编译器的"人物表",记录所有变量、函数的信息。
2.2 符号表存储什么?
每个符号(变量、函数等)的信息:
| 符号名 | 类型 | 作用域 | 位置 | 其他信息 |
|---|---|---|---|---|
x |
int | 全局 | 第5行 | 数组?参数? |
add |
function | 全局 | 第10行 | 返回int,参数(int, int) |
i |
int | for循环内 | 第20行 | 循环变量 |
2.3 符号表的构建
让我们看一个例子:
def calculate_area(width, height):
area = width * height
return area
# 使用
result = calculate_area(10, 20)
符号表构建过程:
sequenceDiagram
participant 编译器
participant 符号表
编译器->>符号表: 遇到函数 calculate_area
Note over 符号表: 添加:calculate_area<br/>类型:function<br/>参数:(width, height)
编译器->>符号表: 遇到参数 width
Note over 符号表: 添加:width<br/>类型:未知(待推断)<br/>作用域:calculate_area
编译器->>符号表: 遇到参数 height
Note over 符号表: 添加:height<br/>类型:未知
编译器->>符号表: 遇到变量 area
Note over 符号表: 添加:area<br/>类型:未知<br/>作用域:calculate_area
编译器->>符号表: 遇到变量 result
Note over 符号表: 添加:result<br/>类型:未知<br/>作用域:全局
2.4 代码实现
class SymbolTable:
"""符号表 - 记录所有符号的信息"""
def __init__(self):
self.symbols = {} # 符号字典
self.scope_stack = ['global'] # 作用域栈
def enter_scope(self, scope_name):
"""进入新的作用域"""
self.scope_stack.append(scope_name)
def exit_scope(self):
"""退出当前作用域"""
self.scope_stack.pop()
def current_scope(self):
"""获取当前作用域"""
return '.'.join(self.scope_stack)
def add_symbol(self, name, symbol_type, **attrs):
"""添加符号"""
full_name = f"{self.current_scope()}.{name}"
self.symbols[full_name] = {
'name': name,
'type': symbol_type,
'scope': self.current_scope(),
**attrs
}
print(f"✅ 添加符号: {full_name} ({symbol_type})")
def lookup(self, name):
"""查找符号 - 从当前作用域向外查找"""
# 从内到外查找
for i in range(len(self.scope_stack), 0, -1):
scope = '.'.join(self.scope_stack[:i])
full_name = f"{scope}.{name}"
if full_name in self.symbols:
return self.symbols[full_name]
return None
# 使用示例
symtab = SymbolTable()
# 添加全局变量
symtab.add_symbol('PI', 'float', value=3.14159)
# 进入函数作用域
symtab.enter_scope('calculate_area')
# 添加参数和局部变量
symtab.add_symbol('width', 'int', is_param=True)
symtab.add_symbol('height', 'int', is_param=True)
symtab.add_symbol('area', 'int')
# 查找符号
print(symtab.lookup('width')) # 找到
print(symtab.lookup('PI')) # 找到(全局)
print(symtab.lookup('xyz')) # None(不存在)
symtab.exit_scope()
三、类型检查:确保类型匹配
3.1 什么是类型?
类型就像标签,告诉编译器数据是什么:
graph TD
A[数据] --> B[类型]
B --> C[int 整数]
B --> D[float 浮点数]
B --> E[string 字符串]
B --> F[bool 布尔值]
B --> G[自定义类型]
C --> H[1, 2, 3, -5, 100]
D --> I[3.14, -0.5, 2.0]
E --> J["hello", 'world']
F --> K[true, false]
3.2 为什么需要类型检查?
错误的类型操作可能导致:
- 程序崩溃:
x = "hello"
y = x + 10 # ❌ 类型错误:字符串不能加整数
- 逻辑错误:
if "hello": # ⚠️ 总是真,可能不是你想要的
print("Always true")
- 内存问题:
int arr[5];
arr[10] = 100; // ❌ 数组越界
3.3 类型检查规则
规则1:算术运算
int + int → int ✅
int + float → float ✅
int + string → ERROR ❌
类型推导树:
graph TD
PLUS["+ 操作"] --> INT1["int: 1"]
PLUS["+ 操作"] --> INT2["int: 2"]
style PLUS fill:#fff4e1
style INT1 fill:#e8f5e9
style INT2 fill:#e8f5e9
result["结果类型: int"] --> PLUS
style result fill:#d4edda
规则2:比较运算
int == int → bool ✅
int == float → bool ✅
int == string → ERROR ❌(某些语言允许,但不推荐)
规则3:赋值
int x = 10 ✅ 类型匹配
int x = "hello" ❌ 类型不匹配
3.4 类型检查的实现
class TypeChecker:
"""类型检查器"""
def __init__(self, symbol_table):
self.symtab = symbol_table
def check_binary_op(self, op, left_type, right_type):
"""检查二元运算的类型"""
# 算术运算:+ - * /
if op in ['+', '-', '*', '/']:
# 两个都是数字类型
if self.is_numeric(left_type) and self.is_numeric(right_type):
# 如果有一个是 float,结果是 float
if left_type == 'float' or right_type == 'float':
return 'float'
return 'int'
# 字符串拼接
if op == '+' and left_type == 'string' and right_type == 'string':
return 'string'
# 类型错误
raise TypeError(f"不支持的运算: {left_type} {op} {right_type}")
# 比较运算:== != < > <= >=
if op in ['==', '!=', '<', '>', '<=', '>=']:
# 类型必须兼容
if self.is_comparable(left_type, right_type):
return 'bool'
raise TypeError(f"无法比较: {left_type} {op} {right_type}")
# 逻辑运算:and or
if op in ['and', 'or']:
if left_type == 'bool' and right_type == 'bool':
return 'bool'
raise TypeError(f"逻辑运算需要布尔值: {left_type} {op} {right_type}")
def check_assignment(self, var_name, expr_type):
"""检查赋值语句"""
symbol = self.symtab.lookup(var_name)
if symbol is None:
raise NameError(f"变量未定义: {var_name}")
var_type = symbol['type']
# 类型兼容性检查
if not self.is_assignable(expr_type, var_type):
raise TypeError(
f"类型不匹配: 变量 '{var_name}' 是 {var_type},"
f"但表达式是 {expr_type}"
)
return True
def is_numeric(self, type_name):
"""是否是数字类型"""
return type_name in ['int', 'float']
def is_comparable(self, type1, type2):
"""两个类型是否可比较"""
# 相同类型
if type1 == type2:
return True
# 数字类型可以互相比较
if self.is_numeric(type1) and self.is_numeric(type2):
return True
return False
def is_assignable(self, from_type, to_type):
"""是否可以赋值"""
# 相同类型
if from_type == to_type:
return True
# int 可以赋值给 float
if from_type == 'int' and to_type == 'float':
return True
return False
# 使用示例
symtab = SymbolTable()
symtab.add_symbol('x', 'int')
symtab.add_symbol('y', 'float')
symtab.add_symbol('name', 'string')
checker = TypeChecker(symtab)
# 测试算术运算
print(checker.check_binary_op('+', 'int', 'int')) # int
print(checker.check_binary_op('+', 'int', 'float')) # float
print(checker.check_binary_op('+', 'string', 'string')) # string
try:
checker.check_binary_op('+', 'int', 'string') # ❌ 报错
except TypeError as e:
print(f"❌ 错误: {e}")
# 测试赋值
checker.check_assignment('x', 'int') # ✅
try:
checker.check_assignment('x', 'string') # ❌ 报错
except TypeError as e:
print(f"❌ 错误: {e}")
四、作用域:变量的可见范围
4.1 什么是作用域?
想象你在公司:
- 全局作用域:公司公告,所有人都能看到
- 部门作用域:部门内部消息,只有本部门能看到
- 个人作用域:你的私人物品,只有你能用
作用域定义了变量在哪里可见(可以访问)。
4.2 作用域的类型
graph TD
A[全局作用域] --> B[函数作用域 main]
A --> C[函数作用域 calculate]
B --> D[块作用域 if语句]
B --> E[块作用域 for循环]
C --> F[块作用域 while循环]
style A fill:#e1f5ff
style B fill:#fff4e1
style C fill:#fff4e1
style D fill:#e8f5e9
style E fill:#e8f5e9
style F fill:#e8f5e9
4.3 作用域规则
规则1:内部可以访问外部变量
PI = 3.14 # 全局变量
def calculate_area(radius):
# 可以访问全局变量 PI
return PI * radius * radius
规则2:外部不能访问内部变量
def my_function():
x = 10 # 局部变量
print(x) # ❌ 错误:x 在这里不可见
规则3:同名变量遮蔽(Shadowing)
x = 100 # 全局 x
def my_function():
x = 200 # 局部 x,遮蔽了全局 x
print(x) # 输出 200
print(x) # 输出 100(全局 x)
4.4 作用域检查的实现
class ScopeChecker:
"""作用域检查器"""
def __init__(self):
self.scopes = [{}] # 作用域栈,初始有全局作用域
def enter_scope(self):
"""进入新作用域"""
self.scopes.append({})
print(f"📥 进入作用域(深度: {len(self.scopes)})")
def exit_scope(self):
"""退出作用域"""
if len(self.scopes) > 1:
self.scopes.pop()
print(f"📤 退出作用域(深度: {len(self.scopes)})")
def declare(self, name, var_type):
"""声明变量"""
current_scope = self.scopes[-1]
if name in current_scope:
raise NameError(f"重复声明: {name}")
current_scope[name] = var_type
print(f"✅ 声明变量: {name} ({var_type})")
def lookup(self, name):
"""查找变量 - 从内到外"""
for i in range(len(self.scopes) - 1, -1, -1):
if name in self.scopes[i]:
scope_level = i
print(f"🔍 找到变量 {name}(作用域层级: {scope_level})")
return self.scopes[i][name]
raise NameError(f"未定义的变量: {name}")
def check_usage(self, name):
"""检查变量使用是否合法"""
try:
var_type = self.lookup(name)
return True
except NameError as e:
print(f"❌ {e}")
return False
# 使用示例
checker = ScopeChecker()
# 全局作用域
checker.declare('PI', 'float')
checker.declare('count', 'int')
# 进入函数作用域
print("\n--- 进入函数 my_function ---")
checker.enter_scope()
checker.declare('x', 'int')
checker.declare('y', 'int')
# 检查变量访问
checker.check_usage('x') # ✅ 局部变量
checker.check_usage('PI') # ✅ 全局变量
# 退出函数作用域
checker.exit_scope()
# 检查变量访问(x 已不可见)
print("\n--- 在全局作用域访问 x ---")
checker.check_usage('x') # ❌ 不可见
五、常见的语义错误
5.1 未定义的变量
print(unknown_var) # ❌ NameError: 未定义
检测方法:在符号表中查找,找不到则报错。
5.2 重复定义
x = 10
x = 20 # ⚠️ 某些语言允许重新赋值
int x = 10;
int x = 20; // ❌ C/C++ 不允许重复声明
检测方法:在同一个作用域内检查是否已声明。
5.3 类型不匹配
int x = "hello" # ❌ 类型不匹配
检测方法:类型检查。
5.4 函数参数不匹配
def add(a: int, b: int) -> int:
return a + b
add(1, 2, 3) # ❌ 参数太多
add(1) # ❌ 参数太少
add("1", "2") # ❌ 参数类型错误
5.5 不可达代码(死代码)
def example():
return 10
print("永远不会执行") # ❌ 死代码
检测方法:控制流分析。
六、实战:完整的语义分析器
让我们实现一个完整的语义分析器:
from enum import Enum
from dataclasses import dataclass
from typing import Dict, List, Optional
class SymbolType(Enum):
"""符号类型"""
VARIABLE = "variable"
FUNCTION = "function"
PARAMETER = "parameter"
@dataclass
class Symbol:
"""符号信息"""
name: str
type_name: str # int, float, string, bool, function
symbol_type: SymbolType
scope: str
line: int
# 函数专用
params: Optional[List[str]] = None
return_type: Optional[str] = None
class SemanticAnalyzer:
"""完整的语义分析器"""
def __init__(self):
self.symbol_table: Dict[str, Symbol] = {}
self.scope_stack: List[str] = ['global']
self.errors: List[str] = []
def current_scope(self) -> str:
"""获取当前完整作用域路径"""
return '.'.join(self.scope_stack)
def enter_scope(self, scope_name: str):
"""进入新作用域"""
self.scope_stack.append(scope_name)
def exit_scope(self):
"""退出作用域"""
if len(self.scope_stack) > 1:
self.scope_stack.pop()
def add_symbol(self, name: str, type_name: str,
symbol_type: SymbolType, line: int,
params: List[str] = None, return_type: str = None):
"""添加符号到符号表"""
full_name = f"{self.current_scope()}.{name}"
# 检查重复定义
if full_name in self.symbol_table:
self.errors.append(
f"第 {line} 行: 重复定义 '{name}'"
)
return False
# 添加符号
self.symbol_table[full_name] = Symbol(
name=name,
type_name=type_name,
symbol_type=symbol_type,
scope=self.current_scope(),
line=line,
params=params,
return_type=return_type
)
print(f"✅ 添加符号: {full_name} ({type_name})")
return True
def lookup(self, name: str) -> Optional[Symbol]:
"""查找符号 - 从当前作用域向外查找"""
# 从内到外查找
for i in range(len(self.scope_stack), 0, -1):
scope = '.'.join(self.scope_stack[:i])
full_name = f"{scope}.{name}"
if full_name in self.symbol_table:
return self.symbol_table[full_name]
return None
def check_variable_usage(self, name: str, line: int) -> bool:
"""检查变量使用是否合法"""
symbol = self.lookup(name)
if symbol is None:
self.errors.append(
f"第 {line} 行: 未定义的变量 '{name}'"
)
return False
print(f"✅ 变量使用合法: {name} ({symbol.type_name})")
return True
def check_assignment(self, var_name: str, expr_type: str, line: int) -> bool:
"""检查赋值语句"""
symbol = self.lookup(var_name)
if symbol is None:
self.errors.append(
f"第 {line} 行: 未定义的变量 '{var_name}'"
)
return False
# 类型检查
if not self.is_assignable(expr_type, symbol.type_name):
self.errors.append(
f"第 {line} 行: 类型不匹配 - "
f"变量 '{var_name}' 是 {symbol.type_name},"
f"但表达式是 {expr_type}"
)
return False
print(f"✅ 赋值合法: {var_name} = {expr_type}")
return True
def check_function_call(self, func_name: str,
arg_types: List[str], line: int) -> Optional[str]:
"""检查函数调用"""
symbol = self.lookup(func_name)
if symbol is None:
self.errors.append(
f"第 {line} 行: 未定义的函数 '{func_name}'"
)
return None
if symbol.symbol_type != SymbolType.FUNCTION:
self.errors.append(
f"第 {line} 行: '{func_name}' 不是函数"
)
return None
# 检查参数数量
if len(arg_types) != len(symbol.params):
self.errors.append(
f"第 {line} 行: 函数 '{func_name}' 期望 "
f"{len(symbol.params)} 个参数,但得到 {len(arg_types)} 个"
)
return None
# 检查参数类型
for i, (expected, actual) in enumerate(zip(symbol.params, arg_types)):
if not self.is_assignable(actual, expected):
self.errors.append(
f"第 {line} 行: 函数 '{func_name}' 第 {i+1} 个参数 "
f"期望 {expected},但得到 {actual}"
)
return None
print(f"✅ 函数调用合法: {func_name}({', '.join(arg_types)})")
return symbol.return_type
def is_assignable(self, from_type: str, to_type: str) -> bool:
"""检查是否可以赋值"""
if from_type == to_type:
return True
if from_type == 'int' and to_type == 'float':
return True
return False
def report_errors(self):
"""报告所有错误"""
if self.errors:
print("\n❌ 语义错误:")
for error in self.errors:
print(f" {error}")
else:
print("\n✅ 没有语义错误!")
# 使用示例
analyzer = SemanticAnalyzer()
print("=== 分析示例程序 ===\n")
# 全局变量
print("--- 全局变量 ---")
analyzer.add_symbol('PI', 'float', SymbolType.VARIABLE, 1)
analyzer.add_symbol('count', 'int', SymbolType.VARIABLE, 2)
# 函数定义
print("\n--- 函数定义 ---")
analyzer.add_symbol(
'add', 'function', SymbolType.FUNCTION, 4,
params=['int', 'int'],
return_type='int'
)
# 进入函数作用域
print("\n--- 进入函数 add ---")
analyzer.enter_scope('add')
analyzer.add_symbol('a', 'int', SymbolType.PARAMETER, 4)
analyzer.add_symbol('b', 'int', SymbolType.PARAMETER, 4)
# 检查变量使用
print("\n--- 检查变量使用 ---")
analyzer.check_variable_usage('a', 5)
analyzer.check_variable_usage('PI', 5)
# 检查赋值
analyzer.check_assignment('a', 'int', 6)
# 退出函数作用域
analyzer.exit_scope()
# 检查函数调用
print("\n--- 检查函数调用 ---")
analyzer.check_function_call('add', ['int', 'int'], 10)
analyzer.check_function_call('add', ['int', 'string'], 11) # ❌ 错误
analyzer.check_function_call('unknown', ['int'], 12) # ❌ 错误
# 报告错误
analyzer.report_errors()
# 打印符号表
print("\n=== 符号表 ===")
for name, symbol in analyzer.symbol_table.items():
print(f"{name}: {symbol}")
6.1 输出示例
=== 分析示例程序 ===
--- 全局变量 ---
✅ 添加符号: global.PI (float)
✅ 添加符号: global.count (int)
--- 函数定义 ---
✅ 添加符号: global.add (function)
--- 进入函数 add ---
✅ 添加符号: global.add.a (int)
✅ 添加符号: global.add.b (int)
--- 检查变量使用 ---
✅ 变量使用合法: a (int)
✅ 变量使用合法: PI (float)
✅ 赋值合法: a = int
--- 检查函数调用 ---
✅ 函数调用合法: add(int, int)
❌ 语义错误:
第 11 行: 函数 'add' 第 2 个参数期望 int,但得到 string
第 12 行: 未定义的函数 'unknown'
七、语义分析与其他阶段的关系
7.1 编译流程图
flowchart TD
A[源代码] --> B[词法分析]
B --> C[Token 流]
C --> D[语法分析]
D --> E[语法树 AST]
E --> F[语义分析]
F --> G[符号表构建]
F --> H[类型检查]
F --> I[作用域检查]
G --> J[中间代码生成]
H --> J
I --> J
J --> K[代码优化]
K --> L[目标代码生成]
L --> M[可执行文件]
style F fill:#fff4e1
style E fill:#e8f5e9
style J fill:#d4edda
7.2 语义分析的位置
| 阶段 | 输入 | 输出 | 任务 |
|---|---|---|---|
| 词法分析 | 源代码 | Token流 | 分词 |
| 语法分析 | Token流 | AST | 检查语法 |
| 语义分析 | AST | 标注的AST | 检查语义 |
| 中间代码生成 | 标注的AST | IR | 生成中间表示 |
八、总结
8.1 核心概念回顾
- 符号表:记录所有变量、函数的信息
- 类型检查:确保类型匹配
- 作用域:变量的可见范围
- 语义错误:未定义、重复定义、类型不匹配等
8.2 为什么语义分析重要?
- ✅ 提前发现错误(编译时 vs 运行时)
- ✅ 提高代码质量
- ✅ 为后续优化提供信息
- ✅ 提供更好的错误信息
8.3 实际应用
| 语言 | 类型检查方式 | 例子 |
|---|---|---|
| C/C++ | 静态类型 | 编译时检查 |
| Java | 静态类型 | 编译时检查 |
| Python | 动态类型 | 运行时检查 |
| TypeScript | 静态类型(可选) | 编译时检查 |
| Rust | 静态类型 + 所有权 | 编译时检查 |
扩展阅读
- 《编译原理》(龙书)- 第5章
- 《Engineering a Compiler》- Type Checking
- Type Systems - Wikipedia
- Symbol Table - GeeksforGeeks
语义分析是编译器的"理解"阶段,让代码不仅有形式,更有意义。