跳转至

4 Semantic Analysis

语义分析是编译器前端最后一个阶段,建立在语法分析生成的 AST 之上,负责检查程序的深层语义是否合法——名字是否已声明、类型是否匹配、作用域是否正确——并为后续的中间代码生成做准备。

4.1 Overview

语义分析建立在 AST 之上,通过遍历 AST 节点检查程序含义。与语法分析处理"结构是否正确"不同,语义分析回答"含义是否合法"。

核心任务:

  • 名字解析 (Name Resolution):将标识符的使用点连接到其声明点。例如变量 x 出现在表达式中,需要找到它是在哪个 var 或 let 中声明的。
  • 类型检查 (Type Checking):验证表达式的类型是否与上下文兼容。如 if 条件必须是整型,+ 两边必须是同类型等。
  • 作用域管理 (Scope Management):遵循语言的块结构和嵌套规则,确保内层声明遮蔽外层同名声明。
  • 翻译 (Translation):将富语义的 AST 降级为结构更简单的中间表示 (IR),供后端使用。这一部分在 Tiger 编译器中体现为将 AST 翻译为 IR Tree。

与语法分析的关系:

语法分析 语义分析
输入 Token 流 AST
理论基础 上下文无关文法 (CFG) 类型系统 / 属性文法
处理对象 结构 (句子形式) 含义 (名字、类型)
能否检测 括号不匹配 变量未声明、类型不匹配
不能检测 int x = "hello" (结构合法) —

语义分析的核心数据结构是符号表 (Symbol Table),核心算法是 AST 的递归遍历 + 类型推导。

4.2 Symbol Table

符号表是语义分析的基础设施,用于实现环境 (Environment)——将名字映射到其属性(类型、作用域层级、内存偏移等)的数据结构。

4.2.1 Formal Definition of Environment

环境 \(\Gamma\) (Gamma) 是一个从名字到绑定的部分函数:

\(\Gamma : \text{Name} \rightharpoonup \text{Binding}\)

其中 Binding 包含该名字的全部语义信息:

Binding = (type: Type, scope_level: int, offset: int, ...)

核心接口:

操作 签名 语义
insert insert(Γ, name, binding) → Γ' 在 Γ 中新增绑定,返回新环境
lookup lookup(Γ, name) → Binding 查找名字在当前环境中的绑定
beginScope beginScope(Γ) → Γ 进入新作用域
endScope endScope(Γ) → Γ 退出当前作用域,恢复外层环境

beginScope / endScope 可以嵌套——进入内层作用域后 insert 的绑定只在该作用域内可见,退出后自动失效。

4.2.2 Imperative Implementation

核心思想:维护一张可变哈希表 + 一个辅助栈。哈希表保证 \(O(1)\) 查找;栈记录每次 insert 操作,用于 endScope 时回滚。

数据结构:

HashTable symtab;       // name → Binding
Stack<Entry> scopeStack;  // 记录 (名字, 被覆盖的旧值),用于回滚

void beginScope() {
    scopeStack.push(SCOPE_MARKER);  // 压入作用域边界标记
}

void insert(string name, Binding info) {
    Binding old = symtab.contains(name) ? symtab[name] : NONE;
    symtab[name] = info;             // 插入 (可能覆盖外层同名)
    scopeStack.push({name, old});    // 保存旧值,NONE 表示本层新增
}

Binding lookup(string name) {
    return symtab[name];             // O(1) 直接查表
}

void endScope() {
    while (scopeStack.top() != SCOPE_MARKER) {
        Entry e = scopeStack.pop();
        if (e.old == NONE)
            symtab.remove(e.name);   // 本层新增的,直接删除
        else
            symtab[e.name] = e.old;  // 恢复被覆盖的外层值
    }
    scopeStack.pop();  // 弹出 SCOPE_MARKER
}

过程示例:

beginScope()              scopeStack=[◆]
insert("x", Ty_Int)       scopeStack=[◆, ("x",NONE)]              symtab={"x": int}
insert("y", Ty_String)    scopeStack=[◆, ("x",NONE), ("y",NONE)]  symtab={"x": int, "y": string}
  beginScope()            scopeStack=[..., ◆]
  insert("x", Ty_Record)  scopeStack=[..., ◆, ("x",int)]          symtab={"x": record, "y": string}
                                                            ↑ 保存旧值 int
  lookup("x") = record    ← 找到内层 x (遮蔽外层)
  endScope()              pop ("x",int) → 恢复 symtab["x"]=int; pop ◆
                          → symtab={"x": int, "y": string}
lookup("x") = int         ← 外层 x 恢复
endScope()                pop ("y",NONE)→remove y; pop ("x",NONE)→remove x; pop ◆
                          → symtab={}

复杂度分析:

操作 时间复杂度 说明
lookup \(O(1)\) 哈希表直接查询
insert \(O(1)\) 哈希表插入 + 栈压入
beginScope \(O(1)\) 压入标记
endScope \(O(k)\) \(k\) 为该作用域内的绑定数

优缺点:查找极快,但 endScope 需要逐条撤销、且多线程下需要同步。

4.2.3 Functional Implementation

核心思想:使用持久化数据结构 (Persistent Data Structure)——每次操作产生新表,旧表保持不变,新旧表共享大部分节点。

数据结构:平衡二叉搜索树 (如 AVL 树或红黑树),每个节点存 (key, value, left, right)。

使用路径复制技术:"复制从根节点到被插入节点的父节点的所有节点",从而可以避免完整拷贝所有旧版本,因此只有 \(O(\log(n))\) 的节点是多余的

上面这个例子就是插入节点 z 的例子,插入到 t 的右边,于是复制相应的父亲节点

持久化操作:

hashTable insert(hashTable t, string name, Binding b) {
    // 沿树路径创建新节点,O(log n)
    // 返回新树的根,旧树 t 保持不变
}

Binding lookup(hashTable t, string name) {
    // 标准 BST 查找,O(log n)
}

作用域管理:函数式实现的作用域管理极其简单——beginScope 直接返回当前表指针(不做任何修改),endScope 切换回进入前保存的旧表指针。

hashTable current = empty;     // 当前环境

void beginScope() {
    // 不需要做任何事 — 只需记住 current 指针
    // 后续 insert 会创建新表,current 指向最新版本
}

hashTable saved = current;     // 进入前保存
insert("x", Ty_Int);           // current 指向新表
insert("y", Ty_String);        // current 指向更新表
// ...
current = saved;               // endScope: O(1) 恢复旧表

endScope 只需将 current 指回旧版本——旧版本仍然完好,O(1) 完成回退。

对比总结:

命令式 函数式
核心数据结构 哈希表 + 栈 持久化 BST
lookup \(O(1)\) \(O(\log n)\)
insert \(O(1)\) \(O(\log n)\)
endScope \(O(k)\) (逐条撤销) \(O(1)\) (切换指针)
多线程 需要同步 天然线程安全
内存 紧凑 路径复制,额外开销

4.2.4 Scope and Shadowing

遮蔽规则 (Shadowing):内层作用域声明的变量会覆盖外层同名的可见性。

let
  var x := 5          // 外层 x: int
in
  let
    var x := "hello"  // 内层 x: string — 遮蔽外层 x
  in
    print(x)          // 引用内层 x, 类型为 string
  end;
  print(x)            // 引用外层 x, 类型为 int
end

实现遮蔽的关键:insert 时新绑定覆盖旧绑定的表项(命令式中直接覆写哈希表;函数式中新节点在查找路径上先被找到),lookup 天然返回最新(最近作用域)的绑定。退出作用域时旧绑定自动恢复。

4.3 Type System

类型系统为程序中的值赋予分类标签(类型),并根据这些分类定义合法的操作组合。

4.3.1 Purpose of Types

维度 说明 示例
开发效率 高层抽象,IDE 自动补全和 API 搜索 Haskell 的 Hoogle: 按类型签名搜索函数
运行性能 静态绑定消除运行时类型检查,优化内存布局 C 的结构体字段偏移在编译时确定
安全可靠 内存安全、功能正确性 Rust 的所有权系统防悬垂指针;Refinement Types 约束取值范围

静态 vs 动态类型检查:

静态检查 动态检查
检查时机 编译时 运行时
典型语言 C, Java, Rust, Haskell Python, JavaScript, Ruby
优点 提前发现错误,无运行时开销 灵活,开发快捷
缺点 类型标注繁琐 运行时错误,性能开销

Tiger 采用静态类型检查。

4.3.2 Tiger Type System

Tiger 语言的类型层次:

Ty
├── Ty_Int          // 整型
├── Ty_String       // 字符串
├── Ty_Record       // 记录类型 {field1: type1, field2: type2, ...}
├── Ty_Array        // 数组类型 array of type (元素类型存储为成员)
├── Ty_Name         // 具名类型别名 (解析后指向上述某具体类型)
├── Ty_Nil          // nil 类型 (兼容所有记录类型)
└── Ty_Void         // 无返回值类型 (语句/过程的返回值)

基本类型:

  • int:32 位有符号整数。比较运算(=, <>, <, <=, >, >=)返回 int(0 假,非 0 真)。

  • string:字符串字面量。仅支持比较运算,无拼接操作。

构造类型:

  • Record:{field1: type1, field2: type2, ...}。字段顺序敏感({x:int, y:int} 与 {y:int, x:int} 不同)。通过 . 访问字段。

  • Array:array of element_type。通过 [index] 访问元素,索引必须是 int 表达式。

特殊类型:

  • nil:空引用,兼容所有记录类型。nil 可以赋值给任何记录类型变量,也可以与任何记录类型比较。但不能赋给 int 或 string。

  • void:不产生值的表达式(如 while、for、赋值语句、if 无 else 分支)的类型。

4.3.3 Type Equivalence

名字等价 (Name Equivalence):两个类型等价,当且仅当它们由同一次类型声明定义。

结构等价 (Structural Equivalence):两个类型等价,当且仅当它们具有相同的结构(递归展开到基本类型)。

Tiger 采用名字等价。

type a = {x: int, y: int}
type b = {x: int, y: int}    // a 和 b 名字不同 → 不等价 !

type c = a                   // c 是 a 的别名 → c 和 a 等价

var va : a := nil
var vb : b := nil
va := vb                     // 类型错误! a ≠ b (名字等价)

var vc : c := nil
va := vc                     // 正确! a = c (同源声明)

Why Name Equivalence?

名字等价实现简单(直接比较类型名或声明标识),并防止"偶然结构相同但语义不同"的类型混用。例如 type Dollars = int 和 type Pounds = int 在名字等价下是不同类型,可防止货币单位混淆。缺点是需要显式类型声明。

等价性判定算法:对名字等价,只需将类型声明展开为"原始类型"后比较。实现中维护一个类型环境 tenv,Ty_Name(symbol) 在 tenv 中查找得到实际类型,递归解开直至非 Ty_Name。

4.3.4 Recursive Types

Tiger 中类型可以递归引用自身,但必须通过 record 或 array 间接进行:

type list = {head: int, tail: list}       // ✓ 通过 record 递归
type tree = array of tree                 // ✓ 通过 array 递归
type a = a                                // ✗ 直接递归 (Tiger 不允许)

区分"递归类型"和"单纯引用别名":

  • 递归类型:类型体内部直接或间接通过 record/array 引用自身,如 type list = {head: int, tail: list} —— list 的体部包含 list 本身。这是合法的递归类型,因为 record/array 在 Tiger 中是引用语义(存的是指针),大小固定。
  • 单纯引用:type b = a 只是给已有类型起别名,b 的体部不包含 b 自身——b 和 a 指向同一个类型,没有产生新结构。
type list = {head: int, tail: list}   // 递归类型 ✓ — 体部通过 record 引用自身
type tree = array of tree             // 递归类型 ✓ — 体部通过 array 引用自身
type a = a                            // 直接递归 ✗ — 无限展开,无法确定大小
type b = c                            // 单纯引用 (别名) — b 只是 c 的别名
type c = {x: int}                     //

Forward Reference

相互引用的类型声明(如 type a = b; type b = {x: a})需要两遍扫描处理前向引用问题:第一遍预注册所有类型名作为占位符,第二遍在完整的名字环境下解析体部。

4.4 Type Checking: Formalization & Implementation

类型检查器的任务:在类型环境 \(\Gamma\) 下,递归遍历 AST,为每个子表达式推导类型,同时检查类型一致性。

核心判断形式:\(\Gamma \vdash e : \tau\) — "在环境 \(\Gamma\) 下,表达式 \(e\) 的类型为 \(\tau\)"。

4.4.1* Typing Rules

推导规则以自然演绎风格书写,横线上方是前提、下方是结论。以下按类别列出 Tiger 的完整类型推导规则:

基本类型与变量:

                        Γ(x) = τ
(T-Int) ───────    (T-Var) ────────    (T-String) ────────────    (T-Nil) ────────
        Γ ⊢ n : int         Γ ⊢ x : τ             Γ ⊢ "s" : string          Γ ⊢ nil : nil

nil 可隐式匹配任意 record 类型——若 if 分支或赋值左右一侧为 nil 另一侧为 record,则视为兼容。

算术与比较(均要求两侧 int,结果 int;Tiger 中比较返回 0/1):

        Γ ⊢ e₁ : int    Γ ⊢ e₂ : int
(T-Op) ──────────────────────────────    (+ - * / = <> < <= > >= 共 10 条同模式规则)
             Γ ⊢ e₁ op e₂ : int

控制流:

    Γ ⊢ e₁ : int    Γ ⊢ e₂ : τ    Γ ⊢ e₃ : τ
(T-If) ─────────────────────────────────────────    (T-While) 条件 int,整体 void
              Γ ⊢ if e₁ then e₂ else e₃ : τ

变量绑定(先处理声明得到 \(\Gamma'\),扩展环境后检查体部;退出作用域时恢复):

    Γ ⊢ decls : Γ'    Γ ⊕ Γ' ⊢ e : τ                   Γ ⊢ e : τ    Γ(x) = τ
(T-Let) ───────────────────────────────    (T-Assign) ───────────────────
         Γ ⊢ let decls in e end : τ                    Γ ⊢ x := e : void

Record 与 Array:

    Γ ⊢ eᵢ : τᵢ (对所有字段)          Γ ⊢ e₁ : array of τ    Γ ⊢ e₂ : int
(T-Record) ─────────────────────    (T-Sub) ───────────────────────────
           Γ ⊢ R{...} : R                     Γ ⊢ e₁[e₂] : τ

函数调用(实参数量与类型必须匹配形参):

    Γ ⊢ f : (τ₁, ..., τₙ) → τᵣ    Γ ⊢ e₁ : τ₁    ...    Γ ⊢ eₙ : τₙ
(T-Call) ─────────────────────────────────────────────────────────
                       Γ ⊢ f(e₁, ..., eₙ) : τᵣ

序列表达式(类型取最右一项):

    Γ ⊢ e₁ : τ₁    Γ ⊕ (x : τ₁) ⊢ e₂ : τ₂
(T-Seq) ──────────────────────────────────
         Γ ⊢ (e₁; e₂) : τ₂

4.4.2 From Rules to Code

类型检查器在 AST 上递归下降,每个推导规则对应 transExp 中的一个 case。类型信息自底向上流动:先递归检查子节点、收集子类型,再按规则判定当前节点类型。

两条环境链:

  • venv:变量名 → 类型(var / let / 形参 时扩展,退出作用域恢复)
  • tenv:类型名 → 实际类型(type 声明时扩展)
Type transExp(ExpNode* e, Env* venv, Env* tenv) {
    switch (e->kind) {
        case INT:    return Ty_Int;                         // (T-Int)
        case STRING: return Ty_String;                      // (T-String)
        case NIL:    return Ty_Nil;                         // (T-Nil)

        case VAR:                                           // (T-Var)
            if (!lookup(venv, e->name))
                error("undefined: %s", e->name);
            return lookup(venv, e->name);

        case BINOP:                                         // (T-Op)
            Type lt = transExp(e->left, venv, tenv);
            Type rt = transExp(e->right, venv, tenv);
            if (lt != Ty_Int || rt != Ty_Int)
                error("%s requires int operands", e->op);
            return Ty_Int;

        case IF:                                            // (T-If)
            if (transExp(e->cond, venv, tenv) != Ty_Int)
                error("if condition must be int");
            Type then = transExp(e->then, venv, tenv);
            Type els  = transExp(e->else, venv, tenv);
            if (then != els) {
                if (isNil(then) && isRecord(els)) return els;   // nil 兼容
                if (isNil(els) && isRecord(then)) return then;
                error("branch types mismatch");
            }
            return then;

        case LET:                                           // (T-Let)
            Env* saved = venv;
            transDecs(e->decs, venv, tenv);                 // 扩展 venv/tenv
            Type body = transExp(e->body, venv, tenv);
            venv = saved;                                   // 恢复 ← 关键
            return body;

        case ASSIGN:                                        // (T-Assign)
            Type lhs = transExp(e->lhs, venv, tenv);
            Type rhs = transExp(e->rhs, venv, tenv);
            if (lhs != rhs)
                error("assignment type mismatch");
            return Ty_Void;

        case CALL:                                          // (T-Call)
            FuncTy* f = lookup(venv, e->func);
            for (int i = 0; i < f->arity; i++)
                if (transExp(e->args[i], venv, tenv) != f->param[i])
                    error("arg %d type mismatch", i);
            return f->result;
    }
}

错误恢复:遇到类型错误不终止,而是返回一个默认类型(如 Ty_Int)或 Ty_Error,让检查器继续处理剩余代码,防止级联报错(一个未定义变量引发后续所有使用处都报错)。

4.4.3 Worked Example

逐步推导以下 Tiger 表达式的类型:

let
  type point = {x: int, y: int}
  var  p : point := point{x = 10, y = 20}
  var  d : int   := p.x + p.y
in
  if d > 0 then d else 0
end
步骤 内容 推导
1 type point = ... point → {x:int, y:int} 加入 tenv
2 var p : point := point{x=10, y=20} (T-Record):字段 10:int, 20:int 匹配 x, y ✓;表达式类型 point 匹配声明 ✓;venv = {p: point}
3 var d : int := p.x + p.y p.x:int, p.y:int;(T-Op) int+int → int;匹配声明 int ✓;venv = {p:point, d:int}
4 if d > 0 then d else 0 (T-Op) d>0:int;(T-If) then d:int else 0:int,两分支一致 ✓ → 体部类型 int
5 整体 let ... end (T-Let) 体部 int → 整体 int ✓

Comparison

名字解析 类型检查 作用域管理
输入 AST 中的标识符引用 AST 中的表达式节点 块结构(let/函数体)
核心结构 符号表 (名字 → 绑定) 类型环境 (Γ) + 推导规则 作用域栈 / 持久化表
输出 每个引用指向其声明 每个表达式标注类型 局部变量在退出时失效
关键算法 递归遍历 + lookup 递归遍历 + 自底向上类型推导 beginScope/endScope
典型错误 未定义变量、重复声明 类型不匹配、操作数类型错误 —

语义分析的核心思想可以概括为:在 AST 上递归遍历,用符号表维护作用域环境,用类型推导规则自底向上计算每个子表达式的类型,并在此过程中检查类型一致性。

评论区

对你有帮助的话请给我个赞和 star => GitHub stars
欢迎跟我探讨!!!