7.4 声明、绑定、遮蔽与导入
声明(declaration)引入一个名称;绑定(binding)将该声明连接到一个语义符号;使用则解析到一个绑定。这些步骤听起来相近,却必须保持分离。let count = 0; 声明了 count。名字解析器(resolver)为它分配 Symbol#42,并将声明绑定到该符号。之后每一个 count 的使用都得到 Symbol#42,即使另一个文件中也有拼写为 count 的变量。
下表把这三个概念分开:
| 概念 | 是什么 | 例子 |
|---|---|---|
| 声明 | 引入一个名称的语法 | let count = 0; |
| 绑定 | 名称与语义符号的对应 | count -> Symbol#42 |
| 使用 | 解析到某个绑定的出现 | print(count) 中的 count |
三者分离才能解释“同名不同物”:两个拼写相同的名称可能是两个独立符号,而同一个符号也可能被许多使用引用。
遮蔽是一项语言策略
let mode = "fast";
{
let mode = "safe";
print(mode);
}
print(mode);第一个 print 解析到内层符号,第二个解析到外层符号。这是词法遮蔽。它适合短暂的转换过程,却也可能掩盖错误,尤其是在遮蔽参数或导入名称时。应分别决定:允许遮蔽、对其警告,还是禁止其中的某些情况。不要将它与同一作用域内的重复声明混淆,后者通常应是错误。
前向引用需要明确规则。函数声明通常在整个模块中可见,以支持互相递归;变量则可能只有在初始化式之后才可见。类型、宏和导入各自也可能有不同的可见阶段;一次性“把每个名字都放进映射”通常不够准确。
导入带来模块级作用域问题
导入不只是文本包含。import math as m 会创建指向模块的绑定 m;from math import sqrt 会在导入模块中创建绑定 sqrt;通配符导入可能创建许多绑定和冲突。应在解析依赖模块的主体前解析其导入项,刻意检测导入环(import cycle),并为每个被导入符号记录来源模块和声明范围。
局部声明、导入、内置和再导出之间可能发生名称冲突。应定义确定性的策略,再让诊断解释相互竞争的来源。导入环可以对声明合法、却对初始化不合法;那是运行时/模块设计规则,而不是一次简单的映射查找。
提示
应把未解析名称、已解析符号、歧义导入和错误符号表示为不同状态。若把所有失败都压缩为 null,每个后续趋都得猜测究竟发生了什么。
模块解析需要自己的图
将模块视为图节点,导入视为有向边。一个模块只加载一次;解析导入时将它标为 loading,只有导出接口已就绪后才标为 loaded。遇到指向 loading 模块的边便识别出一个环。语言随后决定:环是非法、只允许用于声明,还是支持延迟初始化。
例如,geometry 可以导出 Point,draw 导入它并导出 render。如果 geometry 又仅为了运行顶层初始化器而导入 render,这个环可能导致导出尚未初始化。这不是语法分析器错误。一条有用的诊断应给出导入路径、请求的符号以及它尚不可用的阶段。
导入策略清单
- 导入是按源码顺序处理、提升,还是先于局部声明解析?
- 局部声明能否遮蔽被导入名称,是否应对此给出警告?
- 再导出是否足够明确,以便诊断展示来源模块?
- 名字解析器是否区分缺失模块、缺失导出和歧义符号?