Carbon 语言不完全接口(Incomplete Interface)能力边界全解析:前向声明规则、允许操作与循环引用实战
【免费下载链接】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 语言泛型体系中的一个核心设计问题:当一个接口(interface)或命名约束(named constraint)只被前向声明、尚未给出定义(即"不完全")时,究竟可以对它做哪些操作、不可以做哪些操作。该规则源自提案 p002347-what-can-be-done-with-an-incomplete-interface.md,其最终落点为泛型设计文档 Generics details 中的 "Forward declarations and cyclic references" 章节。阅读本文后,你将掌握:前向声明与"不完全"实体的精确语义、可/不可用场景的完整清单(含代码级示例)、如何用前向声明构建图结构等循环引用、如何在接口参数列表自引用时用命名约束绕开限制,以及编译器(toolchain/check)如何用诊断信息落实这些规则。
1. 问题背景:为什么要"不完全接口"
1.1 约束(constraints)概念的引入
提案首先明确了一个术语约定:文中讨论的"约束"指接口(interface)与命名约束(named constraint)两类实体。接口与命名约束在这一规则下不需要区别对待——对两者采用同一套规则,有助于语言保持简单与统一。
从语法上看,一个接口或命名约束的定义由"声明 + 花括号{ ... }包裹的函数体"构成;而**前向声明(forward declaration)则是"声明 + 分号;"。前向声明是一种承诺:被声明的实体稍后一定会被定义。从实体的首次声明(可能是前向声明,也可能是定义的开头部分)开始,直到定义结束为止,该接口或实现被称为不完全(incomplete)**的。
1.2 递归与引用环的驱动
支持前向声明约束的根本动机,是允许在约束被允许定义之前就使用它,典型场景是递归或引用环(reference cycle)。例如声明一张图的边(edge)与节点(node)接口时,二者天然互相引用:边接口需要引用节点类型,节点接口又需要引用边类型,必然构成循环依赖。
然而,某些使用方式要求约束的定义可见才能完成检查。例如访问接口成员、验证关联常量赋值等。这些检查在某些情况下可以推迟,但代价是编译器实现复杂度上升。本提案的目标,就是精确划定"哪些使用是允许的",特别是补齐先前提案 前向声明提案 #1084(Generics details 9: forward declarations)未覆盖的情况。该案例如今对应仓库中的泛型设计文档 details.md 之 "Forward declarations and cyclic references" 章节。
补充:在 Carbon 语义检查器(SemIR 检查阶段)的实现中,"不完全"状态与"正在定义"状态是区分的。见 type_completion.cpp 中
DiagnoseIncompleteInterface、DiagnoseIncompleteNamedConstraint等函数:is_being_defined()对应"当前正在定义"(例如接口定义体内引用自身),否则即"前向声明过"。
2. 核心规则:不完全接口能做什么、不能做什么
以下规则已正式进入 docs/design/generics/details.md 的 "Forward declarations and cyclic references" 章节,是该提案的最核心产出。
2.1 前向声明接口/命名约束的总体约束
一个接口或命名约束可以被前向声明,但受以下规则约束:
- 定义必须与声明位于同一文件中。(详情见第 4 节的备选方案讨论)
- 只有第一次声明可以带访问控制关键字(如
private)。 - 不完全接口或命名约束可以被用作类型、函数、接口或命名约束声明中的约束。这包括接口或命名约束内部的
require声明;但不包括指定关联常量值——因为那涉及对不完全约束进行名字查找。 - 使用不完全接口或命名约束作为签名、去定义泛型函数体是非法的。
- 使用不完全接口或命名约束作为签名、去调用泛型函数是非法的。
- 对不完全接口或命名约束做任何名字查找都是错误。例如用
MyInterface.MemberName访问接口成员、或用where子句约束成员,都是非法的(关于where子句见 details.md 的 "where constraints" 小节)。
2.2 允许清单(✅)
设C为不完全接口或命名约束的名称,则以下上下文合法:
| 允许的用法 | 代码形态 | 说明 |
|---|---|---|
| 检查绑定中的约束 | T: C | T为检查绑定(checked binding) |
| 约束组合 | C & D | 可能因C、D冲突而无效,但只有在二者都完整后才能发现 |
| 要求实现 | interface ... { require ... impls C; }或constraint ... { require ... impls C; } | 实现C所隐含的内容在C完整之前不可见 |
| 蕴含检查 | T: C且T impls C | T为检查绑定 |
| 组合蕴含检查 | T: A & C且T impls C | T为检查绑定;包含需要T impls C的构造,如T as C或U: C = T |
| 前向声明实现 | impl ... as C; | 对C所有关联常量赋值正确性的检查,会推迟到C完整之后 |
2.3 禁止清单(❌)
同样设C为不完全接口或命名约束:
| 禁止的用法 | 代码形态 | 原因 |
|---|---|---|
| 成员访问 | T: C且T.X | 需要查看C的定义才能确认X是否存在 |
带where的约束 | T: C where ... | 需要在C内查找成员 |
| 类作用域扩展实现 | class ... { extend impl as C; } | extend声明要求目标作用域是完整的(见 member_access.md 的 extend 小节) |
| 接口内扩展要求 | interface ... { extend require impls C; }或constraint ... { extend require impls C; } | 同上 |
| 蕴含其他约束的检查 | T: C且T impls A(A不同于C) | 需要看C的定义才能知道它是否蕴含A |
| 实现定义 | impl ... as C { ... } | 关联常量赋值无法在声明时得到验证 |
需要强调一个容易混淆的点:允许
impl ... as C;(带分号的前向声明),但禁止impl ... as C { ... }(带函数体的实现定义)。前者把关联常量检查推迟到C完整;后者则必须在定义C之前就能完成验证,因此非法。
2.4 编译器如何诊断这些违规
这些规则并非纸面设计,而是在 Carbon 编译器检查器中有对应实现。以 type_completion.cpp 为例:
InterfaceIncompleteWithinDefinition:"interface is currently being defined"——接口在其自身定义内被使用;InterfaceForwardDeclaredHere:"interface was forward declared here"——用于定位前向声明的源位置;NamedConstraintIncompleteWithinDefinition/ 对应前向声明诊断——命名约束的同类情况;- 实现相关诊断
ImplAsIncompleteFacetTypeDefinition:"definition of impl as incomplete facet type"——定义实现但不完全接口,见 impl.cpp 中对该诊断的触发点。
在检查器的集成测试中,这些规则被逐条验证,例如:
- toolchain/check/testdata/interface/incomplete.carbon:覆盖
extend require impls A报RequireImplsUnidentifiedFacetType、接口定义体内extend require报RequireImplsIncompleteFacetType、以及impl {} as I where .T = ...对不完全类型C报IncompleteTypeInConversion等场景; - toolchain/check/testdata/impl/incomplete.carbon:验证
impl {} as I {}(对不完全接口 I 定义实现)报错,并附 "interface was forward declared here" 定位提示; - toolchain/check/testdata/impl/forward_decls.carbon:验证前向声明实现的合法用法。
单独运行这些测试的方式(详见各测试文件头部注释):
bazel test //toolchain/testing:file_test \ --test_arg=--file_tests=toolchain/check/testdata/interface/incomplete.carbon bazel run //toolchain/testing:file_test \ -- --dump_output --file_tests=toolchain/check/testdata/interface/incomplete.carbon3. 实战:用前向声明构建循环引用的图结构
3.1 边与节点的经典示例
设计文档给出了一组完整的可编译示例(见 details.md "Example of declaring interfaces with cyclic references")。核心思想:Node接口有一个受约束为"实现Edge"的关联 facetEdgeT,Edge接口有一个受约束为"实现Node"的关联 facetNodeT,且二者互为对方的原始类型。这种双向约束无法直接写出,于是先命名并前向声明无法直接表述的约束:
// 前向声明接口,用于约束的参数列表 interface Edge; interface Node; // 前向声明命名约束,用于接口定义 private constraint EdgeFor(N: Node); private constraint NodeFor(E: Edge); // 用命名约束定义接口 interface Edge { let NodeT: NodeFor(Self); fn Head(self) -> NodeT; } interface Node { let EdgeT: EdgeFor(Self); fn Edges(self) -> DynArray(EdgeT); } // 接口已定义完整,此时才可以引用接口成员, // 因此现在定义命名约束是合法的 constraint EdgeFor(N: Node) { extend Edge where .NodeT = N; } constraint NodeFor(E: Edge) { extend Node where .EdgeT = E; }执行流程分三步:
- 前向声明接口
Edge、Node(用于后面约束的参数列表); - 前向声明命名约束
EdgeFor、NodeFor(用于接口定义体内let ...: ...的类型约束); - 先定义接口,再回头定义命名约束——因为命名约束体
extend Edge where .NodeT = N涉及对Edge成员的where约束,而接口只有在完整后其成员才可见、才允许被where引用。
3.2 该方案的已知局限与未来方向
设计文档也坦承该方案有局限(以Future work标注):例如在interface Node定义体内,编译器只知道EdgeT可转换为type,可能不足以满足作为DynArray参数的要求。若未来此问题成为痛点,可能扩展"不完全接口/类型"的能力,让上面代码无需额外私有约束即可直接写出:
interface Node; interface Edge { let NodeT: Node where .EdgeT = Self; fn Head(self) -> NodeT; } interface Node { let EdgeT: Movable & Edge where .NodeT = Self; fn Edges(self) -> DynArray(EdgeT); }这属于设计文档中标注的未来工作方向,当前并未落地。
3.3 实现细节:前向声明实现与where _语法
除接口/约束前向声明外,设计文档还配套规定了实现(impl)的前向声明("Declaring implementations" 小节),要点如下:
- 实现的定义必须在同一库中;可以在同一文件,或声明在 API 文件、定义在 impl 文件;
- 若既有前向声明又有定义,只有第一次声明必须用
where子句指定关联常量赋值;后续声明可用where _省略; - 不能前向声明一个"不完全接口"的实现——这保证
impl声明中的关联常量赋值可以在声明时被验证; - 为满足一致性(coherence),同一文件内任何匹配到 impl 查找查询的
impl声明,必须在查询之前声明(定义或前向声明均可),这符合 信息累积原则。
设计文档 "Declaration examples" 小节给出了一段完整对照示例(节选),展示接口前向声明、类内内联定义、where _复用等组合:
interface Interface1; interface Interface2; interface Interface3; interface Interface4; class MyClass; // ❌ 非法:不能为不完全接口声明实现 // impl MyClass as Interface1; interface Interface1 { let T1: type; } interface Interface2 { let T2: type; } interface Interface3 { let T3: type; } interface Interface4 { let T4: type; } // 类外前向声明实现(必须带关联常量赋值) impl MyClass as Interface1 where .T1 = i32; impl MyClass as Interface2 where .T2 = bool; class MyClass { // 内联定义此前声明的 impl:无需重复关联常量赋值 impl as Interface1 where _ { } // extend impl 只允许出现在类作用域的声明上 extend impl as Interface3 where .T3 = f32 { } } // 定义此前声明的实现(API 或 impl 文件均可) impl MyClass as Interface2 where _ { }匹配(matching)与一致(agreeing)的规则同样在文档中给出:接口/命名约束的声明在名称解析后同名即匹配;一致要求引入关键字相同、参数列表类型与顺序相同(参数名可省略,若两处都写则必须相同)。实现声明的匹配要求类型、接口表达式及forall子句(如有)均匹配。
4. 权衡与备选方案:为什么是这些规则
4.1 备选一:允许声明与定义分离在不同文件(被否决)
曾考虑允许接口或命名约束的声明写在 API 文件、定义写在同库的 impl 文件。相关用例讨论见 #931 提案:泛型 impl 访问细节 的 "Private interfaces in public API files" 小节,以及 issue #971。在 2022-10-24 的 #generics-and-templates 讨论中,社区决定这些用例可以接受,甚至允许私有接口的定义也放在 API 文件中。但为了能够用文件内局部信息去检查不完全接口的非法使用,最终重申了 #971 的决定并沿用 #1084 提案的要求:定义必须与声明在同一文件。这一约束同时服务于第 3.1 节的需求——正因为组合约束C & D的冲突检查被推迟,才必须保证编译器在推进到定义处时能够拿到全部信息并报错。
4.2 备选二:对不完全约束的where子句不做专门限制(被否决)
曾考虑不为"不完全约束上的where子句"设立专门规则,理由是那些真正有问题的情况(如访问约束成员的where .X = T)已经被禁止了。去掉该规则后,像C where Vector(.Self) is Hashable这类写法(C不完全)也会被允许。最终决定:在出现明确动机用例之前保持更严格的规则,即禁止不完全约束上的where子句。
4.3 备选三:禁止组合不完全约束(C & D)(被否决)
曾考虑禁止用&组合不完全约束,因为无法立刻诊断两个约束间的冲突。例如:
constraint C; constraint D; // 无法判断 `C` 与 `D` 是否冲突。 fn FT:! C & D; // 下面两个定义是冲突的。 constraint C { extends I where .X = i32; } constraint D { extends I where .X = bool; }但现有使用前向声明构建"边/节点图"循环引用的示例恰恰需要这一特性,因此决定支持它。这反过来成为"定义必须与声明同文件"要求的动因:冲突错误可以在编译器推进到定义处时被检测出来。冲突只能在二者都完整后被发现的代价,则由第 2.2 节允许清单中的注释明确承担。
5. 原理落地与验证:从设计到实现的证据链
5.1 实现证据
本提案的规则在 Carbon 检查器(toolchain/check)中有明确落点:
- 不完全状态的建模与诊断:
DiagnoseIncompleteInterface、DiagnoseIncompleteNamedConstraint(见 type_completion.cpp),区分"正在定义"与"仅前向声明"两种不完全形态; - 不完全 facet 类型定义实现的拦截:
ImplAsIncompleteFacetTypeDefinition(见 impl.cpp); - 前向声明语法的解析与 AST 处理:
interface、constraint等前向声明由语义检查器中对应 handler 处理(参见 toolchain/check 目录下handle_interface.cpp、handle_impl.cpp等文件); - 约束间的匹配与一致规则:文档 "Matching and agreeing" 小节(details.md)定义了接口/约束/实现三者的匹配判定。
5.2 测试证据
- toolchain/check/testdata/interface/incomplete.carbon:对
extend require impls A、定义体内的extend require、不完全类型参与转换等场景给出期望诊断输出; - toolchain/check/testdata/impl/incomplete.carbon:验证对不完全接口定义实现(
impl {} as I {})报错、对不完全接口前向声明实现(impl C as I;)时名字解析失败的场景; - toolchain/check/testdata/impl/forward_decls.carbon:正向验证前向声明实现的合法路径与关联常量赋值约束。
这些测试使用仓库的 file_test 框架(见 toolchain/testing 与 testing/file_test),通过// CHECK:STDERR注释断言精确的诊断文本与源码位置,是上述规则"事实即代码"的完整证据。
6. 小结
"不完全接口能做什么"这一设计问题的答案,最终沉淀为一套对称的允许/禁止清单:允许在声明与约束组合中使用不完全约束(包括&组合、require impls、前向声明impl),禁止任何需要成员可见性的操作(成员访问、where子句、extend、蕴含检查、实现定义体)。其底层动机是表达力与实现简单性的平衡——这正是 Carbon 语言目标中 "code that is easy to read, understand, and write" 的要求。围绕该提案的三次备选方案取舍(文件内同文件定义、where限制、C & D组合)共同塑造了最终规则,而 type_completion.cpp 等实现文件与interface/incomplete.carbon等测试文件则让每条规则都可验证、可追溯。
【免费下载链接】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),仅供参考