前言

在上一篇文章中,我们讲了语法分析如何把代码变成一棵树。但这还不够。

就像这个句子:

“无色的绿色思想愤怒地睡觉”

语法完全正确(形容词+形容词+名词+副词+动词),但毫无意义

编译器也一样。即使代码语法正确,也可能语义错误

"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 语义分析要做什么?

主要任务:

  1. 建立符号表:记录所有变量、函数的信息
  2. 类型检查:确保类型匹配(整数加整数,不是整数加字符串)
  3. 作用域检查:变量在哪里可用
  4. 其他检查:未定义变量、重复定义、死代码等

二、符号表:编译器的记事本

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 为什么需要类型检查?

错误的类型操作可能导致:

  1. 程序崩溃
x = "hello"
y = x + 10  # ❌ 类型错误:字符串不能加整数
  1. 逻辑错误
if "hello":  # ⚠️ 总是真,可能不是你想要的
    print("Always true")
  1. 内存问题
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 核心概念回顾

  1. 符号表:记录所有变量、函数的信息
  2. 类型检查:确保类型匹配
  3. 作用域:变量的可见范围
  4. 语义错误:未定义、重复定义、类型不匹配等

8.2 为什么语义分析重要?

  • ✅ 提前发现错误(编译时 vs 运行时)
  • ✅ 提高代码质量
  • ✅ 为后续优化提供信息
  • ✅ 提供更好的错误信息

8.3 实际应用

语言 类型检查方式 例子
C/C++ 静态类型 编译时检查
Java 静态类型 编译时检查
Python 动态类型 运行时检查
TypeScript 静态类型(可选) 编译时检查
Rust 静态类型 + 所有权 编译时检查

扩展阅读


语义分析是编译器的"理解"阶段,让代码不仅有形式,更有意义。