Carbon 语言命名空间中的名称绑定澄清:P003407 提案解析与实现验证
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
导读
本篇文章以 Carbon Language 仓库中的提案 p003407-clarify-name-bindings-in-namespaces.md 为核心,系统梳理 Carbon 语言中"名称绑定(binding pattern)"与"命名空间(namespace)"相互作用的语法与语义规则:包括命名空间成员只能在声明命名空间的同一名称作用域内声明、绑定模式中多个名称必须同属一个命名空间、以及命名空间只能声明在文件作用域等关键约束。读完本文,你将理解这些规则的设计动机、完整语法示例,并能通过仓库中的设计文档与 toolchain 测试用例验证其真实行为。
背景:Carbon 的命名空间与绑定模式
命名空间(Namespaces)
在 Carbon 中,命名空间为实体提供结构化命名路径,其设计集中在 代码与名称组织设计文档 中。namespace关键字的语法可以粗略表示为如下正则:
namespace NAME_PATH;命名空间通过名称前缀作用于其他实体,例如:
package Time; namespace Timezones.Internal; struct Timezones.Internal.RawData { ... } fn ParseData(data: Timezones.Internal.RawData);一个namespace声明会把名称路径中的第一个标识符加入文件(library)的命名空间中。在上例中,声明namespace Timezones.Internal;之后,Timezones成为一个可用标识符,而Internal需要通过Timezones访问。
值得注意的是,命名空间可以跨库合并,并且可以从其他包导入;但即使某命名空间已存在于当前包导入的库中,若要向其添加符号,仍必须在本地再次声明它(参见 Redeclaring imported namespaces)。
绑定模式(Binding Patterns)
绑定模式是模式匹配体系的一部分,详细规范见 pattern_matching.md 的 Binding patterns 小节。一个名称绑定模式声明一个由标识符指定的绑定:
binding-pattern ::= `ref`? (identifier `:` expression | `self` (`:` expression)?) binding-pattern ::= (`generic` | `template`)? identifier `:` expression pattern ::= binding-pattern绑定模式具有"阶段(phase)"属性:
- 运行时绑定模式(runtime binding pattern):绑定到运行时的动态值,是显式函数参数和局部绑定的默认方式;
- 检查型泛型绑定模式(checked generic binding pattern):绑定到符号常量(symbolic constant),即类型检查时未知的编译期值,是推导型函数参数和编译期实体参数的默认方式;
- 模板泛型绑定模式(template generic binding pattern):绑定到模板常量,在类型检查时已知,需要使用
template关键字显式声明。
检查型与模板绑定模式统称为编译期绑定模式,它们不能出现在var模式内部。
当var或let声明使用绑定模式来声明名称时,就与命名空间产生了交集——这正是 P003407 提案要澄清的核心问题。
问题:NS.a的歧义与缺失细节
虽然class NS.C这种"平凡情形"看起来已被提案 #107: Code and name organization 顺带支持,但其细节仍然缺失。例如,在绑定多个名称时存在多种语法选择;同时,下列代码没有明确结论:
namespace NS; class ClassT { // 这是通过 `NS` 访问的类成员,还是 `NS` 内的文件作用域成员? // 它的生命周期是什么? var NS.a: i32 = 0; }提案指出:这里var NS.a到底表示"通过 NS 访问的类成员",还是"位于 NS 命名空间内部的文件作用域成员"完全不清晰,其生命周期也无法确定。此外,关于命名空间能否在文件作用域之外声明也存在不确定性。本提案的主要目标就是消除这些歧义。
提案:三条核心规则
P003407 提案确立了三条核心规则:
- 要求命名空间成员在与命名空间声明相同的名称作用域内声明。由于在文件作用域之外声明命名空间已被禁止,这实际上意味着:命名空间成员只能在文件作用域内声明。
- 允许绑定模式直接在命名空间中声明名称(如
NS.a)。 - 禁止在同一模式中向不同命名空间引入绑定。
这三条规则已落入正式设计文档 代码与名称组织 中,其具体语义如下。
规则一:命名空间成员必须在同一作用域内声明
命名空间成员只能在与声明该命名空间相同的名称作用域中声明:
namespace NS; // ✅ 允许:声明位于文件作用域,与 `NS` 的声明相同。 class NS.ClassT { // ❌ 错误:类体有它自己的名称作用域。 var NS.a: i32 = 0; } fn Function() { // ❌ 错误:函数体有它自己的名称作用域。 var NS.b: i32 = 1; } // ✅ 允许:声明位于文件作用域,与 `NS` 的声明相同。 namespace NS.MemberNS; // ✅ 允许:声明位于文件作用域,与 `NS.MemberNS` 的声明相同。 class NS.MemberNS.MemberClassT {}这条规则直接回答了引言中的歧义问题:class ClassT { var NS.a: i32 = 0; }是非法的,因为ClassT的类体是独立名称作用域,而NS是在文件作用域声明的。它同时让成员的生命周期变得清晰:命名空间成员与其命名空间具有相同的声明上下文。
规则二:绑定模式可声明命名空间成员
当一个模式中的绑定模式用于声明名称(如var或let)时,允许使用命名空间限定名。但由于规则一限制了作用域,命名空间限定的模式绑定只能用于var或let声明的模式中(函数参数、模式匹配等其他场景不受影响)。
规则三:同一模式内所有名称必须同属一个命名空间
当一个模式通过绑定模式声明多个名称时,所有名称必须在同一个命名空间中:
namespace NS; // ✅ 允许:`a` 和 `b` 使用默认命名空间。 var (a: i32, b: i32) = (1, 2); // ✅ 允许:`c` 和 `d` 位于同一命名空间。 var (NS.c: i32, NS.d: i32) = (3, 4); // ❌ 错误:`e` 和 `f` 不在同一命名空间。 var (e: i32, NS.f: i32) = (5, 6);需要强调的是,此限制仅适用于绑定模式中声明名称的情形,不适用于模式中名称的其他用法(例如表达式中引用已有实体)。
规则三的实践示例:库级命名空间应用
把上述规则放入真实的库组织场景中,可以更直观地看到它的作用。以 代码与名称组织设计文档 中的导出与调用示例为参照,一个使用命名空间的库可以这样组织:
package Geometry library "Shapes"; namespace TwoDimensional; struct TwoDimensional.Circle { ... } fn TwoDimensional.Area(c: TwoDimensional.Circle) -> f64;调用方通过包实体和命名空间逐级访问:
package Caller; import Geometry library "Shapes"; fn Run() { var c: Geometry.TwoDimensional.Circle = ...; Print(Geometry.TwoDimensional.Area(c)); }而如果要在一个声明中同时初始化多个命名空间成员,规则三要求它们全部属于TwoDimensional:
namespace TwoDimensional; // ✅ 允许:两个成员都在 `TwoDimensional` 内。 var (TwoDimensional.x: f64, TwoDimensional.y: f64) = (0.0, 0.0); // ❌ 错误:`z` 在默认命名空间,`TwoDimensional.w` 在命名空间内。 var (z: f64, TwoDimensional.w: f64) = (0.0, 0.0);这种约束让"名称声明位置"可预测,也使得重构(例如把实体从一个命名空间移动到另一个)对调用方的影响范围更容易分析(参见设计文档中的 Other refactorings 小节)。
源码验证:toolchain 中的规则实现
上述规则不仅是纸面设计,也已在 Carbon 的语义检查器(toolchain/check)中实现并通过测试固定。以下测试文件是理解这些规则真实行为的直接证据。
命名空间只能在文件作用域声明
测试文件 fail_not_top_level.carbon 验证了"命名空间不得在非顶层作用域声明":
fn F() { // 错误:`namespace` declaration not at top level [NamespaceDeclNotAtTopLevel] namespace N; fn N.F() {} } class C { // 错误:`namespace` declaration not at top level [NamespaceDeclNotAtTopLevel] namespace N; fn N.F() {} } interface I { // 错误:`namespace` declaration not at top level [NamespaceDeclNotAtTopLevel] namespace N; fn N.I() {} }在函数体、类体、接口体中声明namespace都会触发诊断NamespaceDeclNotAtTopLevel,直接对应提案中"禁止在文件作用域外声明命名空间"的决定。该文件属于toolchain/testing:file_test驱动的文件测试体系,可以通过如下命令单独运行验证:
bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/namespace/fail_not_top_level.carbon命名空间与局部变量的遮蔽
测试文件 shadowing.carbon 展示了命名空间名可以在局部作用域中被变量遮蔽:
namespace NS; fn Main() { var NS: () = (); NS = (); ... }这从侧面印证了名称作用域的边界:namespace NS位于文件作用域,而局部var NS位于函数体内,两者通过作用域规则自然隔离。这也正是提案反复强调"同一名称作用域"的原因——名称绑定与命名空间声明的归属都必须以作用域为边界来判断。
此外,toolchain/check/testdata/namespace 目录下的其他测试(如fail_duplicate.carbon、fail_conflict_imported_namespace_first.carbon、merging.carbon、nested.carbon等)分别覆盖了命名空间重复声明、导入冲突、跨库合并、嵌套等场景,可作为继续研究命名空间语义的入口。
设计理由
提案的取舍基于 Carbon 的项目目标:代码易于阅读、理解和编写:
- 要求多个名称的声明统一使用
NS.a语法,与单变量情形(如var NS.a)保持一致,降低认知负担; - 要求命名空间成员必须在与命名空间相同的名称作用域内声明,使生命周期更清晰——成员与命名空间拥有相同的声明上下文,读者无需在多个作用域之间跳转推理;
- 禁止混合命名空间可以避免
var (NS.a: i32, b: i32)这类写法中b被误认为也属于NS的困惑。
备选方案与被否决的设计
提案共评估了四种备选方案,其否决理由有助于深入理解最终规则的边界。
备选一:允许用命名空间前缀整个元组绑定模式
var NS.(a: i32, b: i32) = (3, 4);被否决的原因:单个语句声明多个名称的场景本就少见,这种"命名空间限定符与被声明标识符分离"的语法可能成为孤例;相比之下,NS.a与class NS.Class等其他限定场景保持一致,因此最终选用NS.a形式。
备选二:允许绑定模式向多个命名空间声明名称
namespace NS; var (NS.a: i32, b: i32) = InitData();被否决的原因:混合命名空间会造成混淆——例如b可能被误读为声明在NS中。既然没有数据表明这种能力能带来足够收益,为保持简单性,单一声明内禁止混合命名空间。
备选三:允许在非当前作用域所有的命名空间中声明名称
namespace NS; class ClassT { var NS.val: i32; class NS.ChildT {} }被否决的原因最为复杂:这里package.NS.val更像全局变量,而ClassT.NS.val看起来像实例成员;由于NS不在ClassT的名称作用域内,ClassT.NS.val(或instance.NS.val)能否用于引用产生的变量也不明确。这种命名问题同样扩展到非绑定声明(如NS.ChildT)。
禁止用命名空间跨越名称作用域,与"通常禁止在其他名称作用域中声明名称"的既有规则一致,例如:
class A { class B { // `C` 必须直接声明在 `A` 内部。 class A.C; } } // `D` 必须在 `A` 内声明,即使它是单独定义的。 class A.D {}namespace声明与其中的名称必须书写在同一名称作用域内,这避免了名称查找歧义,并让名称作用域边界在不同声明之间保持一致。
备选四:允许在文件作用域之外的任意作用域声明命名空间
提案 #107 的示例都聚焦于文件作用域,其他作用域未被仔细考虑,因此路径语义模糊。虽然成员命名空间在某些场景可能有价值,例如复杂类:
class Complex { namespace OptionSet1; class OptionSet1.MemberClassA; class OptionSet1.MemberClassB; namespace OptionSet2; class OptionSet2.MemberClassC; class OptionSet2.MemberClassD; namespace Vars; var Vars.a; }但本提案明确反对在文件作用域之外声明命名空间,理由有二:
- 提案 #107 只提及文件作用域命名空间,隐含排除了其他作用域;
- 禁止在其他作用域声明命名空间与 C++ 保持一致。
如果允许,则必须进一步决定Complex.Vars.a是实例生命周期还是全局生命周期。目前命名空间只能在文件作用域声明,以保持与 C++ 的一致性;这一决定可能在未来提案中被重新评估。
总结
P003407 提案为 Carbon 语言中"名称绑定 × 命名空间"的交互确立了三条清晰规则:命名空间成员须在与命名空间相同的名称作用域(实际即文件作用域)内声明、绑定模式可直接声明命名空间成员、同一模式内的多个名称必须同属一个命名空间。这些规则不仅消除了class ClassT { var NS.a: i32 = 0; }之类的歧义,还与 C++ 及 Carbon 既有的名称作用域边界保持一致。从 fail_not_top_level.carbon 等测试文件可以看出,规则已经落地为NamespaceDeclNotAtTopLevel等具体诊断并被语义检查器强制实施,读者可以通过文件测试体系自行验证。
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考