Carbon 语言元组与元组索引(Tuples and Tuple Indexing):p003646 提案全解析与实现验证
【免费下载链接】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 语言设计提案 p003646: Tuples and tuple indexing:它正式确立了 Carbon 中元组类型的存在与基本语法规则,并新增了通过数值索引提取元组元素的能力(tuple.0、tuple.(expr)及指针形式的->0、->(expr))。读完本文,你将掌握 Carbon 元组索引的两套完整语法、其背后的上下文相关词法规则、索引边界与拼写约束,以及该设计在工具链源码与测试用例中的落地证据。
提案背景:为什么需要元组索引
提案指出,在 Carbon 之前的设计中,访问元组元素的唯一途径是模式匹配(pattern matching)。虽然模式匹配能覆盖大多数场景,但当只需要单个元素的值时,它显得过于繁琐——因此需要一种更简洁的按数值索引访问方式。
提案同时对比了主流语言的既有做法:
- Python:使用方括号
tup[1]索引元组; - C++:
std::pair用.first/.second,std::tuple用std::get<I>; - Rust 与 Swift:使用
.N(N为十进制整数),且禁止在N中使用数字分隔符与进制前缀(Rust 因历史原因允许部分字面量后缀)。
提案还特别指出:Carbon 现有文档中一直建议使用tuple[i]进行元组索引,但该语法此前从未经过正式提案审批,这正是本提案要补上的缺口。
提案核心:正式确立元组类型
在语法与索引设计之前,提案首先补上了元组类型的"名分":Carbon 中存在元组类型,它们是具名位置元素(unnamed positional elements)的乘积类型(product type)。此前的多份提案虽已隐含支持元组,但从未有一份提案正式声明其存在。
此外,提案将此前仅在 leads issues 中确定、但未形成提案的设计决策正式纳入设计:
- Leads issue #2191(单元素元组及其语法):尽管聚焦于单元素元组,但确立了所有元数的元组语法。Carbon 元组允许可选尾随逗号,而单元素元组强制要求尾随逗号(如
(42,)); - Leads issue #710:确立了元组的赋值、比较与隐式转换规则——这些操作均**逐元素(elementwise)进行,其中关系比较按字典序(lexicographically)**执行。
元组索引语法总览
本提案的核心新增内容是元组索引,支持以下两种语法:
| 语法形式 | 说明 |
|---|---|
.N | N为整数字面量,如tuple.0、pair.1 |
.(expr) | expr为整数类型的模板常量(template constant),如tuple.(1 + 1) |
对于指向元组的指针,同样支持->N与->(expr)两种形式。
该语法设计有两大意图:
- 提供 C++ 中
.first、.second与std::get<I>用法的迁移语法——允许表达式(而非仅字面量)作为索引,可支持std::get<expression>类代码的迁移; - 保持与结构体成员访问语法的一致性,让元组元素索引"看起来像"成员访问。
词法规则详解:.后紧跟数字的词法特判
多层元组索引会产生诸如tuple_of_tuples.1.2的写法。关键问题在于:若按现有词法规则,1.2会被识别为一个实数(real literal),那么tuple_of_tuples.1.2就会被切分为tuple_of_tuples.1.2三个 token,而非两次元组索引。为此,提案引入新规则:
当一个
.或->token 后紧跟数字时,将其词法解析为.或->token 后跟一个整数(integer literal),绝不会解析为实数。
也就是说,词法切分变得轻微上下文相关(contextual):紧跟在.或->之后的 token 词法规则,与其他任何上下文中的词法规则不同。
提案同时给出了一种等价但非上下文相关的表述方式:将.integer(以及->integer)视为一个单一词素(lexeme),但产出两个 token。这种表述对工具链实现更友好——例如语法高亮器可以直接把.i当作一种独立的 token 类型,而无须实现上下文相关的词法。
提案在 Rationale 中强调,这一规则仅需查看数字字面量之前的单个字符即可决定它是"元组索引"还是"通用数字字面量",完全契合 低上下文敏感性原则。
索引即名称:十进制整数作为元素名
提案将元组的各个元素视为以其十进制整数作为名称:.0、.1等。在简单成员访问(simple member access)中,整数的拼写必须精确匹配元素名称,否则即为错误:
// OK let a: i32 = (1, 2, 3).0; // 错误:元组元素名中没有名为 `0x0` 的元素 let b: i32 = (1, 2, 3).0x0; // 错误:元组元素名中没有名为 `1_2` 的元素 let c: i32 = large_tuple.1_2;即进制前缀(如0x)与数字分隔符(如_)均不允许出现在简单成员访问的索引中。但同样的拼写可作为表达式操作数使用(见下文):
// 均为合法:`.()` 形式接受表达式 let x: i32 = (1, 2).(0x0); let y: i32 = large_tuple.(1_2);这一设计在 成员访问设计文档 中有更完整的表述:元组类型的成员名是integer-literal而非 word,每个位置元素的名称即对应十进制整数,成员解析的结果是指向元组对应元素的实例成员:
// ✅ `a == 42`。 let a: i32 = (41, 42, 43).1; // ❌ 错误:没有名为 `0x1` 的元组元素。 let b: i32 = (1, 2, 3).0x1; // ❌ 错误:没有名为 `2` 的元组元素。 let c: i32 = (1, 2).2; var t: (i32, i32, i32) = (1, 2, 3); let p: (i32, i32, i32)* = &t; // ✅ `m == 3`。 let m: i32 = p->2;优先级与结合性
.N语法与后缀成员访问语法.name拥有相同的优先级,并可在同一表达式中自由组合:a.0.x.1合法;.(expr)语法并非本提案新创,继续维持与.name相同的优先级;- 成员访问表达式从左到右结合。优先级上,成员访问高于所有其他表达式形式(如
1 + X.Y解析为1 + (X.Y)),而低于主表达式(字面量、未限定名称、括号表达式)。
表达式操作数:.()形式的语义
在.(expr)语法中:
- 若第一个操作数是元组,第二个操作数是任意整数类型的常量,则结果是对应的元组元素,其效果等同于以十进制整数字面量指定索引;
- 该规则内建于语言本身,
.()索引目前不可重载(not currently overloadable)——这与a[i]下标语法(通过IndexWith/IndirectIndexWith接口重写为方法调用)形成鲜明对比,详见 下标设计文档。
在复合成员访问中,若第二个操作数为整数或整数字面量类型,则要求第一个操作数为元组类型或扩展了元组类型,否则成员解析失败;第二个操作数必须是非负的模板常量且小于元组元素个数:
// ✅ `d == 43`。 let d: i32 = (41, 42, 43).(1 + 1); // ✅ `e == 2`。 let template e: i32 = (1, 2, 3).(0x1); // ❌ 错误:不存在索引为 4 的元组元素。 let f: i32 = (1, 2).(2 * 2); // ✅ `n == 3`。 let n: i32 = p->(e);边界检查
若元组索引不在 0 到(元素个数 − 1)的闭区间内,则索引无效。该约束在检查器测试中得到了严格验证(详见下文实现验证小节)。
元组切片:本提案明确不做
当前骨架设计(skeleton design)曾建议使用tuple[a .. b]对元组进行切片,例如tuple[0 .. 2]提取前两个元素。本提案不覆盖元组切片,但指出未来可通过tuple.(0 .. 2)这类语法补充。同时提案提醒风险:该语法可能引导出关于 Carbon 的错误理论——即"tuple.__给出元素,而tuple.(__)给出一个元组"。切片提案若成行,还应重新审视"从末尾负向索引"(见备选方案)的需求。
设计动机与理由(Rationale)
提案依据 项目目标 给出了设计理由:
| 目标/原则 | 对应设计考量 |
|---|---|
| 语言工具与生态 | 词法规则实现相对简单;语法高亮器等工具可将.i视为独立 token 类型,而无须上下文相关词法 |
| 软件与语言演进 | 统一使用元组字段索引,有助于支持随时间新增元组元素的代码演进 |
| 易读、易懂、易写的代码 | 元组访问比模式匹配更简洁;将.1.2词法解析为四个 token 而非两个,避免链式成员访问难以书写;简单成员访问要求无分隔符的十进制整数,使索引可视为元素名 |
| 与现有 C++ 代码的互操作与迁移 | 为.first、.second、std::get<I>提供迁移语法;允许表达式索引以支持std::get<expression>迁移 |
| 低上下文敏感性原则 | 仅查看数字字面量前一个字符即可决定词法解析方式 |
备选方案讨论
备选词法规则
方案 A:将.0、.1等整体作为单一 token。这会简化词法(不再上下文相关),但被否决,原因有二:与struct.fieldname的处理不一致;且要么tuple . 0非法(与结构体不一致),要么需要为tuple.0单独建立语法产生式。
方案 B:只要前一个 token 是.就词法解析为整数,无论是否紧跟。例如将((1, 2, 3), 4) . 0.1视为元组索引而非"元组后跟.与实数"。Swift 采用此做法。否决原因:此处的0.1看起来像实数,会造成读者困惑;且会令上下文相关词法变为非局部规则。
方案 C:其他达到类似效果的手段,如允许.后的实数再拆分为成员访问(rustc的做法)、或把实数切为"整数 token +.token + 后缀 token"再由解析器合并(intellij-rust 的做法)。提案指出这些方案并非完全等价(如 Rust 中 proc macro 可观察差异),且任何 token 合并/拆分都会导致 token 流与程序解释不匹配,对工具链不友好——例如许多 Rust 语法高亮器无法正确高亮链式元组索引。
十进制索引限制
Carbon 与 Rust、Swift 一致,将元组索引限制为十进制整数。该限制引入了.0x0与.(0x0)之间的不一致(可轻易去掉),但它允许将.0、.1等直接视为元组元素的名字(类似结构体字段名),且进制前缀或数字分隔符并无明确实用价值。
方括号记法
替代方案是tuple[0]与tuple[IndexConstant]。优点是与常量/表达式下标语法更一致;缺点是与结构体成员访问不够一致。提案认为非常量索引的元组访问是罕见操作,且.记法更能传达开发者意图:
x[n]记法主要面向**同质(homogenous)**索引(如数组、容器,通常允许运行时索引);.记法用于**异质(heterogenous)**访问,元组索引要求常量索引,正如结构体成员访问要求常量名称——这体现了二者在求值阶段上的差异。
此外,.N记法未来可扩展为对结构体/类的成员索引([]记法难以支持后者)。[]记法的优势是减少O.0、l.0、Z.0与0.0、1.0、2.0的视觉混淆,但 Rust/Swift 实践中未见此问题,且类似歧义(如F(O, l, Z)与F(0, 1, 2))在无.0后缀时同样存在。
从元组末尾负向索引
Python 风格的tuple.-1或tuple.(-1)(表示"最后一个元素")被否决:这种记法容易混淆且存在尴尬的边缘情况——**差一错误(off-by-one)**或访问"超出起始一个"的元素时,有时会被接受并静默执行错误操作。提案明确:若未来引入元组切片,应重新评估此问题(切片常需要从末尾取元素),并可考虑不同记法如tuple.(.size - 1)。
尾随逗号
Carbon 元组允许可选尾随逗号,单元素元组强制尾随逗号,其备选方案已在 leads issue #2191 中讨论。
实现验证:源码与测试中的落地证据
提案设计已在当前仓库的工具链中实现并测试。最直接的证据是检查器(check)阶段的文件测试 toolchain/check/testdata/tuple/element_access.carbon,其中覆盖了提案定义的几乎全部行为:
- 基础访问:
b.0提取单元素元组(i32,)的第 0 个元素; - 非常量索引表达式:
a.({.index = 1}.index)、a.(0 as i32)均可作为表达式索引使用; - 边界错误:
TupleIndexOutOfBounds——对(i32,)使用b.1、对(i32, i32)使用a.2、对空元组F().0、以及a.(-10)负索引均报错; - 非常量索引报错:
a.(b)(b为运行时变量)报TupleIndexNotConstant,并伴随"对编译期专属函数的非常量调用"错误; - 非整数索引报错:
a.(2.6)报ConversionFailure,提示Core.FloatLiteral无法隐式转换为Core.IntLiteral; - 非元组类型报错:对
array(i32, 2)使用.0报TupleIndexOnANonTupleType,明确"只有元组才能这样索引"。
元组类型本身的基础语义(空元组、嵌套元组、单元素/双元素元组及其复制)由 toolchain/check/testdata/tuple/basics.carbon 验证;其中嵌套元组(((), ()), ())的 SemIR 转储清晰地展示了tuple_access ... element0/element1的嵌套链式访问结构。
从源码结构看,元组元素索引在语义层由SemIR::TupleAccess指令表示,其元素序号使用 toolchain/sem_ir/field.h 中定义的ElementIndex类型(基于IndexBase),并在 toolchain/sem_ir/inst_categories.h 中被列入表达式指令类别——这印证了提案中"元组索引是内建于语言、不可重载的成员访问"这一语义定位:与通过接口重写实现的a[i]下标(见 docs/design/expressions/indexing.md)走的是完全不同的代码路径。
如果你希望本地运行这些测试验证行为,可执行(仓库为只读,仅限测试运行):
bazel test //toolchain/testing:file_test \ --test_arg=--file_tests=toolchain/check/testdata/tuple/element_access.carbon小结
提案 p003646 为 Carbon 语言补上了两块拼图:一是以正式提案的形式确立了元组类型及其基本语义(乘积类型、逐元素赋值/比较/隐式转换、单元素元组强制尾随逗号);二是新增了.N与.(expr)(以及指针的->形式)两套元组索引语法,并配套定义了上下文相关的词法规则、十进制元素命名约束、与成员访问一致的优先级,以及严格的边界检查。它明确拒绝了方括号记法、负向索引与切片语法(留待未来提案),并通过 Rationale 将每项选择锚定到项目目标与低上下文敏感性原则。这些设计决策不仅停留在文档层面,更已在检查器测试与 SemIR 指令层面完整落地,可供语言实现者与工具链开发者直接参照。
【免费下载链接】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),仅供参考