尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Carbon 语言下标语法与语义:IndexWith 与 IndirectIndexWith 接口设计深度解析

Carbon 语言下标语法与语义:IndexWith 与 IndirectIndexWith 接口设计深度解析 Carbon 语言下标语法与语义IndexWith 与 IndirectIndexWith 接口设计深度解析【免费下载链接】carbon-langCarbon Languages 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 的提案 p002274-subscript-syntax-and-semantics.md即 Pull Request #2274系统讲解 Carbon 如何以传统的a[i]方括号语法支持数组array-like与切片slice-like两类语义以及背后的接口设计IndexWith与IndirectIndexWith。文中不仅完整保留了提案中的接口定义、重写规则、blanket final impl 与示例代码还结合当前仓库的 prelude 实现、check/lower 阶段源码与 file_test 测试用例帮助读者理解该提案从设计文档到可运行工具链的完整落地过程。读完本文你将掌握 Carbon 中自定义下标操作的接口实现方法、值类别与类型检查之间的交互以及该设计为一致性coherence所做的关键权衡。背景与问题为什么下标不是内置而是接口从问题说起数组与切片的下标语义差异Carbon 需要一个便捷的方式对数组、切片等对象进行索引。提案中特别定义了切片slice的含义一个引用数组某段区域并提供访问能力的对象相当于 C 中的std::string_view或std::span。切片与数组在下标 API 语义上存在微妙差异对数组进行索引只有当数组本身是 l-value 时结果才是 l-value对切片进行索引无论切片自身的值类别value category是什么结果都可以是 l-value。例如对 r-value 数组取下标只能得到值元素会被拷贝出来而对 r-value 切片取下标依然可以得到可写的引用——因为切片本身就代表一层间接引用indirection正如对指针 r-value 解引用与对指针 l-value 解引用行为一致。两个约束静态开放扩展与一致性Carbon 的静态开放扩展机制只有接口interface因此用户自定义下标必须以接口定义。同时为了支持泛型函数的定义期检查下标操作必须能仅凭接口暴露的信息完成完整类型检查而为了满足一致性coherence只要类型信息足以证明某个下标表达式合法无论编译器掌握多少类型信息该表达式都必须执行同一份代码。核心提案IndexWith与IndirectIndexWith双接口设计下标表达式的语义定位Carbon 采用与 C 相同的约定下标表达式形如_lhs_ [ _index_ ]其优先级与.、-、函数调用相同并且与它们一起从左向右结合。当a是 l-value 时a[i]的结果始终是 l-value但当a是 r-value 时结果可以是 l-value 或 r-value取决于类型实现的接口场景应实现的接口对 r-value 下标产生 r-value 结果如数组IndexWith对 r-value 下标产生 l-value 结果如切片IndirectIndexWithIndirectIndexWith是IndexWith的子类型subtype。下标表达式在重写rewrite时会优先检查类型是否实现IndirectIndexWith若实现则重写为对IndirectIndexWith方法调用否则重写为对IndexWith方法调用。一个类型最多只能实现这两个接口之一IndirectIndexWith通过一个 final blanketimpl提供IndexWith从而保证一致性。接口定义提案原始文本提案中给出的接口定义如下interface IndexWith(SubscriptType:! Type) { let ElementType:! Type; fn Atme: Self - ElementType; fn Addraddr me: Self* - ElementType*; } interface IndirectIndexWith(SubscriptType:! Type) { impl as IndexWith(SubscriptType); fn Addrme: Self - ElementType*; }其中At负责按值取元素Addr负责取元素地址。注意IndirectIndexWith的Addr接收普通me: Self而不是addr me: Self*因为它不要求 Self 是 l-value——这正对应切片 r-value 也可以产生 l-value 下标结果。重写规则rewrite rules设_lhs_的类型为T_index_的类型为I则下标表达式按以下顺序重写若T实现了IndirectIndexWith(I)重写为*(( _lhs_ ).(IndirectIndexWith(I).Addr)( _index_ ))否则若_lhs_是 l-value重写为*(( _lhs_ ).(IndexWith(I).Addr)( _index_ ))否则重写为( _lhs_ ).(IndexWith(I).At)( _index_ )。可以看到重写规则依据值类别 接口实现两个维度分派数组 l-value 走Addr解引用得到可写引用数组 r-value 走At拷贝值切片无论值类别都走IndirectIndexWith.Addr。Blanket final impl保证一致性如果按上述规则直接要求IndirectIndexWith类型自己实现三个方法IndexWith.At、IndexWith.Addr、IndirectIndexWith.Addr会带来两个问题一是实现负担重二是类型作者可能以不一致的方式实现这些方法破坏一致性。提案用final external impl消除这两个问题final external impl forall [SubscriptType:! Type, T:! IndirectIndexWith(SubscriptType)] T as IndexWith(SubscriptType) { let ElementType:! Type T.(IndirectIndexWith(SubscriptType)).ElementType; fn Atme: Self - ElementType { return *(me.(IndirectIndexWith(SubscriptType).Addr)(index)); } fn Addraddr me: Self* - ElementType* { return me-(IndirectIndexWith(SubscriptType).Addr)(index); } }于是实现IndirectIndexWith的类型不必也不能再提供自己的IndexWith.At与IndexWith.Addr两个接口在实现层面被强制统一到IndirectIndexWith.Addr上一致性因此得到保证。值类别作为一致性的一部分该设计有一个重要推论一致性要求必须同时覆盖值类别与类型。具体而言l-value应被视为r-value的一个子类别。这意味着对任何包含 r-value 子表达式的合法表达式只要该 r-value 与某个 l-value 表达式类型相同且求值结果相同用后者替换前者不会改变外层表达式的合法性与语义。由此可以保证一旦知道某个值实现了IndexWith再得知它还实现了IndirectIndexWith不会使先前通过类型检查的代码失效或改变其语义。示例数组、切片与元组下标数组类型实现IndexWithclass Array(template T:! Type) { external impl as IndexWith(like i64) { let ElementType:! Type T; fn Atme: Self - T; fn Addraddr me: Self* - T*; } }切片类型实现IndirectIndexWithclass Slice(T:! Type) { external impl as IndirectIndexWith(like i64) { let ElementType:! Type T; fn Addrme: Self - T*; } }对比可见切片类型只需实现一个Addr而At与 l-value 场景的Addr均由 blanket final impl 补齐。元组下标一个值得注意的近似方案由于当时 Carbon 的元编程metaprogramming与可变参数variadics设计尚未成熟无法泛化地为元组提供下标。但可以针对具体元组类型近似实现例如(i8, f64)external impl (i8, f64) as IndexWith(typeof(0)) { let ElementType:! Type i8; fn Atme: Self - i8; fn Addraddr me: Self* - i8*; } external impl (i8, f64) as IndexWith(typeof(1)) { let ElementType:! Type f64; fn Atme: Self - f64; fn Addraddr me: Self* - f64*; }提案特别指出该方案的两个前提性问题它预设每个整数字面量都有独立类型typeof(0)、typeof(1)这一点当时尚未定论它只支持整数字面量下标不支持普通整数类型的常量值。这在一定程度上与 Carbon 的元编程哲学相悖——Carbon 将类型视为值以便元编程更像普通编程而这里恰恰反过来把值当成了类型。因此最终的元组下标设计很可能与这里展示的方案大相径庭。仓库落地从提案到 prelude 与工具链现状prelude 中的IndexWith简化形态当前仓库在 core/prelude/operators/index.carbon 中实际落地了IndexWith接口SubscriptType参数当前为非模板的type方法仅保留Atpackage Core library prelude/operators/index; interface IndexWith(SubscriptType: type) { let ElementType: type; fn At(self, subscript: SubscriptType) - ElementType; }而 docs/design/expressions/indexing.md设计文档保留了完整的双接口形态只是按后续值类别术语演进将Addr更名为Ref、at addr me改为bound ref self并明确要求这些接口中用于形成持久引用表达式的Ref方法必须以ref返回。设计文档中的完整形态如下interface IndexWith(SubscriptType: type) { let ElementType: type; fn At(bound self, subscript: SubscriptType) - val ElementType; fn Ref(bound ref self, subscript: SubscriptType) - ref ElementType; } interface IndirectIndexWith(SubscriptType: type) { require Self impls IndexWith(SubscriptType); fn Ref(bound self, subscript: SubscriptType) - ref ElementType; }对应的 blanket final impl设计文档形态final impl forall [SubscriptType: type, T: IndirectIndexWith(SubscriptType)] T as IndexWith(SubscriptType) { where ElementType T.(IndirectIndexWith(SubscriptType).ElementType); fn At(bound self, subscript: SubscriptType) - val ElementType { return self.(IndirectIndexWith(SubscriptType).Ref)(index); } fn Ref(bound ref self, subscript: SubscriptType) - ref ElementType { return self.(IndirectIndexWith(SubscriptType).Ref)(index); } }可以看到提案中的核心结构——子类型关系、blanket final impl、At/Ref(Addr) 双方法——在演进后的设计文档中全部保留。当前工具链仍以IndexWith.At为主力实现路径IndirectIndexWith的完整落地留有 TODO见下文。String 的内置下标IndexWith的真实用例仓库中IndexWith最典型的实际使用者是字符串类型。core/prelude/types/string.carbon 中impl forall [T: ImplicitAs(i64)] String as IndexWith(T) where .ElementType Char { fn At(self, subscript: T) - Char string.at; }这行代码表明String对任意可隐式转换为i64的索引类型T实现了IndexWith元素类型为Char方法体是名为string.at的内置函数。check 阶段的语义检查与 lower 阶段的代码生成分别针对该内置函数做专门处理见下文并在 toolchain/check/testdata/operators/overloaded/string_indexing.carbon 与 toolchain/lower/testdata/operators/string_indexing.carbon 中验证了字面量负索引Test[-1]、越界索引Test[4]、错误索引类型Test[C]等诊断以及Test[0]、Test[3]的语义分析与 LLVM IR 生成。check 阶段下标表达式的语义分析下标表达式在语义分析check阶段的处理位于 toolchain/check/handle_index.cppHandleParseNode(Parse::IndexExprStartId, ...)原样保留表达式等待IndexExpr统一处理HandleParseNode(Parse::IndexExprId, ...)先弹出_index_与_lhs_将_lhs_转为值或引用表达式然后按操作数类型分派若类型是SemIR::ArrayType将索引转换为i32类型源码中标注 TODO称将来应替换为基于IndexWith的 impl 查找对数组值插入ValueAsRef后生成SemIR::ArrayIndex并遵循索引持久引用得到持久引用、索引其他值得到值表达式的规则源码同样留有 TODO称将来应替换为IndexWith/IndirectIndexWith的选择其他类型调用PerformIndexWith——构造以索引类型为参数的Operator{IndexWith, At}再交给BuildBinaryOperator完成接口成员查找与调用构造。从源码结构可以推断提案中的重写规则在工具链中对应的正是这类先做 impl 查找、再生成方法调用的机制PerformIndexWith的核心就是从Core.IndexWith中取At关联方法。IndexWith、At均被登记在 toolchain/check/core_identifier.def 的 X-macro 列表中供核心标识符解析使用。check 测试用例接口驱动的下标如何被验证toolchain/check/testdata/operators/overloaded/index_with_prelude.carbon 是理解该机制的最佳样例它包含四个子测试overloaded_index.carbon自定义类C实现Core.IndexWith(SubscriptType)含At随后let x: ElementType c[s];被成功解析为对At的方法调用。从输出的 SEM IR 可以清楚看到bound_method与call %bound_method(...)的生成过程即下标表达式最终被重写为接口方法调用。overloaded_builtin.carbon通过impl (C, C) as Core.IndexWith(Core.IntLiteral)近似模拟元组下标s[0]被解析到针对IntLiteral索引的At。fail_invalid_subscript_type.carbon类C只实现了IndexWith(SubscriptType)而未实现IndexWith(IntLiteral)因此c[0]报出MissingImplInMemberAccess错误cannot access member of interface Core.IndexWith(Core.IntLiteral) in type C that does not implement that interface。fail_index_with_not_implemented.carbon完全未实现IndexWith的类型对c[0]同样报出MissingImplInMemberAccess。此外toolchain/check/testdata/index/fail_invalid_base.carbon 覆盖了下标基表达式不合法的场景对命名空间N[0]、函数F[1]、普通结构体值{.a 1, .b 2}[0]、结构体类型{.a: i32, .b: i32}[0]取下标均报错其中后两者正是由于没有IndexWith实现而失败。这些 file_test 用例可通过以下命令单独运行bazel test //toolchain/testing:file_test \ --test_arg--file_teststoolchain/check/testdata/operators/overloaded/index_with_prelude.carbonC 互操作与代码生成中的IndexWith在 C 互操作路径中toolchain/check/cpp/operators.cpp 将Core.IndexWith接口映射到 Clang 运算符下标clang::OO_Subscript使 C 类型的下标操作可以被 Carbon 语法调用——这正是提案所述C 互操作与迁移目标的实现支撑之一。在 lower 阶段toolchain/lower/testdata/operators/string_indexing.carbon 展示了String下标的 LLVM IR 形态加载字符串结构体{ptr, i64}、extractvalue取出数据指针、getelementptr计算元素地址、load读取Chari8并返回。备选方案与设计权衡不同的下标语法切片之所以与数组语义不同是因为切片像指针一样代表间接引用对切片 l-value/r-value 下标行为一致与对指针 l-value/r-value 解引用行为一致是同理的。因此有人主张给两类语义使用不同语法最朴素的方案只有数组类类型支持下标切片改用解引用即(*s)[i]。但该写法过于繁琐且难以在不循环定义的前提下定义*s的类型。更可行的方案定义独立语法如s*[i]并配套两套互不继承的接口重写不依赖类型信息而只依赖值类别。但这类语法相当陌生且*的解析无论对人还是对工具本就复杂引入新写法得不偿失。最终提案选择复用同一a[i]语法、用接口 重写规则在内部区分两类语义。多下标multiple indices本提案不支持a[i, j]这类逗号分隔的多下标多维数组需要。但a[(i, j)]是可行的——它把元组作为一个下标。多余的括号在语法上略显嘈杂若隐式补括号则会把语法噪音转移到实现侧例如impl Foo as IndexWith((i64,))。提案认为一旦可变参数variadics设计就绪把接口及其方法改成可变参数版会更干净。只读下标本提案没有为类似 Cstd::string_view、std::spanconst T的只读访问类型提供现成路径。虽然数组 r-value 的只读与这些类型的上下文无可变访问权看似相近但二者有本质区别值类别能表达前者真正不可变却不适合表达后者只是当前上下文没有可变权限。提案判断Carbon 未来可能需要类似 Cconst类型系统的机制来解决这类需求而这与本提案基本正交。仅 r-value 下标如 Python 切片本提案不支持返回值的下标操作例如 Python 的a[i:j]或 Swift 的a[i...j]用下标语法构造子切片。若要支持需要另一对按值返回的接口interface RValueIndexWith(SubscriptType:! Type) { let ElementType:! Type; fn Addraddr me: Self* - ElementType; } interface RValueIndirectIndexWith(SubscriptType:! Type) { extends RValueIndexWith(SubscriptType); let ElementType:! Type; fn Addrme: Self - ElementType; } final external impl [SubscriptType:! Type, T:! RValueIndirectIndexWith(SubscriptType)] T as IndexWith(SubscriptType) { let ElementType:! Type T.(RValueIndirectIndexWith(SubscriptType)).ElementType; fn Addraddr me: Self* - ElementType { return me-Addr(subscript); } }这里仍需一对接口加 blanket final impl 强制一致因为在该语境下数组与切片语义依然不同对 r-value 数组取切片非法取切片等价于取地址而 r-value 数组不可取地址对 r-value 切片取切片合法且应当支持。同时还要扩展重写规则去检测并使用这些接口——若T同时实现IndexWith(I)与RValueIndexWith(I)程序应因歧义而 ill-formed这不会导致组合爆炸。但提案也承认不确定这种能力是否值得增加的复杂度。Map 式下标如 C std::map 的隐式插入本提案不支持会向集合插入新元素的下标操作如 Cstd::map/std::unordered_map的m[k]隐式插入因为提案要求可下标类型必须支持对 r-value 下标而 r-value 是不可变的。若要支持需新增仅 l-value 可下标的接口并扩展重写规则。提案对该特性持审慎甚至怀疑态度隐式插入在 C 中并非明确的成功。x m[i];看起来像从m读取却可能写入m容易令即使是资深程序员也忽略而且它无法表达作者意图作者到底假设i已存在还是依赖隐式插入同时x m[i];在m为 const 时无法编译。另一方面m[i] x;可能插入这一点相对不意外但插入本身可能低效——它必须先默认构造新值再赋入x。提案还给出了一个可行的补救方向对m[i] x;语法特殊对待定义独立的Assign方法IndexWith与IndirectIndexWith各加一个Assign让赋值位置的下标走Assign其余下标仍按主提案重写。但该方案会给接口与重写规则增加复杂度例如需要把(m[i]) x与m[i] x视为等价因此被留作潜在的未来扩展。基于用途的重写Usage-based rewriting另一种获取std::map行为的思路当使用上下文只需要 r-value 时即使操作数是 l-value也将下标表达式重写为At而非Addr类似 Rust 依据使用上下文是否可变来选择下标重写。这样类型作者可以让Addr潜在插入而At不插入从而令m[i] x;可插入、x m[i];不可插入。但该方案有明确代价m[i].Method()在Method以指针接收me时也会变成潜在插入并带有与 C 相同的性能缺陷它违背了类型信息应从子表达式流向父表达式而从不反向流动的原则该原则当时已讨论但尚未采纳。该思路的变体是当操作数是 l-value 但上下文只需要 r-value 时允许编译器在At与Addr间自由选择可能带来更高效或更安全的代码也会促使库作者保证At与Addr的一致性。但若编译器在这种情况下可预测地总是选At库作者可能利用这一实现细节以Addr隐式插入而At不插入的方式模拟std::mapAPI——这既带有一切故意支持该特性的缺点又多了用户代码依赖非受支持实现细节的演化风险。设计目标依据下标是过程式编程中的基础操作在性能敏感代码中尤其常见。用简洁、熟悉的语法支持它有助于实现 Carbon 的代码易于阅读、理解与编写与性能关键软件两大目标而对切片类类型也沿用与 C 相同的语法支撑了与现有 C 代码的互操作与迁移目标。相关设计文档 docs/design/expressions/indexing.md 亦将本提案列为其备选方案一节的权威出处。总结a[i]在 Carbon 中不是语言内置的魔法语法而是建立在接口之上的可扩展机制IndexWith表达数组式下标r-value 下标得到值IndirectIndexWith表达切片式下标r-value 下标也能得到引用二者通过子类型关系与 blanket final impl 保证实现的一致性与单一性。从提案文本到 core/prelude/operators/index.carbon、docs/design/expressions/indexing.md、toolchain/check/handle_index.cpp 及其 file_test 用例仓库为这一设计提供了从规范到实现再到验证的完整链条。当前工具链已通过IndexWith.At支撑String下标等真实场景而IndirectIndexWith的完整落地、多下标、只读下标、切片式与 map 式下标等能力则被明确标注为后续演进方向。【免费下载链接】carbon-langCarbon Languages 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),仅供参考
返回列表