Skip to content

第4章:名字绑定、类型与运行时

作用域与符号表

语法正确不保证名字有定义,也不保证操作数类型匹配。语义分析建立声明与使用之间的绑定,检查作用域、类型、返回和控制流规则。

词法作用域按源程序嵌套结构解析名字。进入块时建立新作用域,查询从当前作用域向外查找。遮蔽意味着内层同名声明成为本区域的绑定,不表示外层对象被删除。

text
let x = 1;
{
    let x = 2;
    print(x);
}
print(x);

在通常块作用域规则下,两次输出为 2 和 1。符号表应给两个声明不同的内部身份,不能仅靠字符串 x 表示它们。

类型判断

类型环境 Γ\Gamma 记录名字的类型。整数加法规则可写为

Γe1:intΓe2:intΓe1+e2:int.\frac{\Gamma\vdash e_1:\mathrm{int}\qquad\Gamma\vdash e_2:\mathrm{int}} {\Gamma\vdash e_1+e_2:\mathrm{int}}.

规则描述哪些表达式可接受,不负责证明运行时不会发生所有错误。数组越界、空引用或除零是否静态排除,取决于类型系统和语言设计。

隐式转换也应显式进入 IR,例如整数转浮点。否则后端无法可靠区分值转换与仅重新解释位模式。

生命周期与闭包

作用域说明名字在哪里可见,生命周期说明对象何时存在。函数返回后,局部名字不再可见,但被闭包捕获的对象可能仍需存活。

闭包通常包含代码指针和环境。若内层函数使用外层变量并逃出外层调用,环境不能继续指向已失效的栈帧;可采用堆分配、复制捕获或逃逸分析证明安全的其他方案。

运行时支持

对象分配、垃圾回收、异常展开和动态派发需要编译器与运行时协作。精确垃圾回收可能要求在安全点提供哪些寄存器和栈槽含指针的元数据。

异常处理还要求恢复控制和资源。把可能抛异常的调用越过另一个副作用,可能改变可观察顺序,即使两者的数据依赖看起来独立。

练习

  1. 为上述遮蔽例子建立声明 ID 与使用点的对应关系。
  2. 给出名字作用域结束但对象必须继续存活的例子。
  3. 为什么编译器的栈布局会影响垃圾回收和异常处理?

上次更新: