4.2 符号表:名字的登记处


4.2 符号表:名字的登记处

本节摘要:符号表是编译器全流水线共享的名字数据库,支持登记、查询、作用域进出三种操作。本节实现一张带作用域栈的哈希符号表,处理主线语句四个名字的登记与查询,讲清同名遮蔽、开链哈希的冲突处理,以及符号表在错误恢复期的纪律。

阅读完本节,你应当能够:

  1. 实现带作用域栈的符号表,进出作用域时正确压弹
  2. 解释查询永远命中"最近登记的那个同名项"的机制
  3. 说明符号表被哪些阶段读写、各读写什么字段
  4. 处理恢复期对残缺条目的标记与绕行

名字为什么需要一张表

编译器里的名字是高频查询对象。第 2 章词法分析登记 total 的拼法,第 4 章类型检查要查它的类型,第 6 章代码生成要查它的存储位置——同一个名字,三种消费者。若无集中的表,每阶段各自维护映射,一致性噩梦。符号表的本质是把名字的全部编译期知识集中一处,谁要谁来查。

主线语句涉及四项登记,一项典型的条目长这样:

符号表条目:total 名字拼法 total 类别 变量 类型 float 作用域级别 1(函数体) 存储信息 栈帧偏移 0,4 字节(待第 6 章回填) 引用位置 第 12 行(供交叉引用工具使用)

作用域栈:遮蔽的机制

名字会重复。内层代码块声明的新 total 要遮蔽外层的 total,离开代码块后外层又"复活"。实现机制是作用域栈:进入作用域压一层,离开弹一层,查询从栈顶往下找,命中的自然是最近的。

class Scope: def __init__(self): self.names = {} # 本层:名字 → 条目 class SymbolTable: def __init__(self): self.stack = [Scope()] # 全局层打底 def enter(self): # 进入新作用域 self.stack.append(Scope()) def leave(self): # 离开作用域,整层丢弃 self.stack.pop() def declare(self, name, kind, typ): top = self.stack[-1] if name in top.names: # 同层重复声明是错误 raise SemError(f"重复声明:{name}") top.names[name] = {"kind": kind, "type": typ, "scope": len(self.stack) - 1} def lookup(self, name): for scope in reversed(self.stack): # 从栈顶往下找 if name in scope.names: return scope.names[name] # 最近者优先 = 遮蔽 return None # 找不到 → 未声明错误

用一段带遮蔽的代码验证行为:

st = SymbolTable() st.declare("price", "var", "float") # 全局层 st.declare("qty", "var", "int") st.enter() # 进入代码块 st.declare("qty", "var", "float") # 内层同名,遮蔽外层 int print(st.lookup("qty")["type"]) # float —— 命中最近层 st.leave() # 离开代码块 print(st.lookup("qty")["type"]) # int —— 外层复活

主线语句的四个名字在类型检查前完成登记(声明语句先于赋值被处理),检查器对每个 ID 节点调 lookup,拿到类型属性挂到树上——4.1 节的类型合成正是从这里取的原料。

图 作用域栈与查询路径:最近者胜

图 作用域栈与查询路径:最近者胜

数据结构的选择

符号表的查询密度极高——大型程序每行代码平均触发数次 lookup。候选结构对比:

结构 查询 登记 适用
线性表 教学玩具
有序表加二分 慢(要挪位) 很少用
哈希表 近似常数 近似常数 工业主流
平衡树 对数 对数 需要按序遍历时

哈希表是主流选择,冲突用开链(同桶挂链表)。工程细节两条:散列函数要处理字符串前缀聚集(许多变量共享前缀如 get、set);扩容时机在负载因子过半,重散列的代价一次性付清。另外,leave 弹层时不能真的销毁条目——调试器与交叉引用工具还要用,惯常做法是把整层链表挂进"死亡名单"供后续遍历。

⚠️ 常见坑:函数参数登记在函数体层还是外层,二进制库的 ABI 各有约定,不一致会导致符号冲突。写编译器时这条约定要在符号表设计期就定死,并写进测试用例。

💡 关键直觉:符号表是编译器里少有的"写一次读多次"的组件,值得在查询路径上花力气优化。而错误恢复期对它有一个特殊纪律:报错导致的残缺声明也要登记(标记为错误条目),否则同一个未完成声明会在后续每个引用点重复报"未声明",刷屏淹没真错。

登记处的疑难窗口

问:同名声明在不同层,符号表存几份? 每层各一份,物理上可能是同一张哈希表的不同桶,或每层独立小表加链。查询语义永远是最近层优先。离开内层时整层废弃,外层条目从未被覆盖——遮蔽是查询规则,不是数据覆盖,这保证了弹层后外层名字无损复活。

问:编译结束后符号表就作废了吗? 没有。调试信息(变量名、行号、栈帧布局)就是符号表的子集,被写进目标文件的调试段;链接器还有自己的全局符号表处理跨文件引用。一张登记表服务三任:编译、调试、链接——这也是它值得做扎实的原因。

补一问:模块与命名空间怎么进符号表? 作为作用域的推广:模块名登记在全局层,其内部名字登记在模块层,引用时按限定名逐级查找。实质仍是那套作用域栈规则,只是层级来源从代码块换成了名字空间。跨文件的模块符号要等链接期或模块系统加载后才能解析,那是另一个登记处(链接器符号表)的辖区。

本节要点回顾

  • 集中登记:名字的拼法、类型、存储、引用位置集中一处,全流水线共享
  • 作用域栈:进层压栈、离层弹栈,查询从顶向下、最近者胜即遮蔽
  • 结构选型:哈希加开链是工业主流,扩容在负载因子过半
  • 弹层不销毁:死亡条目挂名单,供调试与交叉引用
  • 恢复期纪律:残缺声明登记为错误条目,防止重复报错刷屏

名字都有着落了。下一节把带类型标注的树翻译成三条三地址码——主线语句的"指令形态"首秀。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U