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
导读
本文以 Carbon Language 的字符串字面量提案为核心,结合当前仓库中的词法规范与 lexer 源码实现,系统梳理 Carbon 字符串字面量的完整语法体系:包括单行字符串与块字符串、完整的转义序列表、以#定制的原始字符串、缩进剥离规则以及 UTF-8 编码语义。读完本文,你将掌握 Carbon 中四种字符串形态(普通/原始 × 单行/多行)的写法与取舍,理解其与 C++、Rust、Swift、Python 的异同,并能从源码与测试用例层面验证每条规则的实际行为。
注意:本文所述语法以当前仓库实际实现为准。原始提案 p000199 曾提议以
"""作为块字符串分隔符,但该语法随后被提案 p001360 变更,当前实现采用'''(三个单引号)作为块字符串定界符,详见下文"语法演进"一节。
问题与背景:为什么要专门定义字符串字面量
提案的出发点很朴素:为"人类可读文本"提供一种书写语法。这里的"文本"含义宽泛——正则表达式、程序源码、C++ mangled name 等最终要交给计算机解释的内容同样属于"文本",它与任意二进制数据的区别在于:文本是由字符序列构成的,通常需要经过某种编码(encoding)才能存储与传输。编码是"文本中的字符序列"与"有界整数值序列(即码元 code units)"之间的双向映射。例如俄语单词 углерод(carbon)在 UTF-8 编码下对应D1 83 D0 B3 D0 BB D0 B5 D1 80 D0 BE D0 B4这一组字节序列。
在考察各语言既有实践时,提案总结了字符串字面量常见的三类扩展能力:
- 转义序列(escape sequences):让字符串能够包含难以输入、对读者有歧义或会导致渲染问题的字符(空白、不可见字符、改变后续字符渲染的字符),也能包含任意码元。C++ 与 Python 支持
\a、\b、\f、\n、\r、\t、\v;JavaScript 去掉\a;Java 再去掉\v;Rust 与 Swift 只保留\n、\r、\t。 - 原始字符串(raw string literals):不识别转义序列,适合嵌入与宿主语言共享转义约定的另一种机器可读语言。Python 用
r前缀,C++ 用r"DELIM(...)DELIM"自定义定界符,Rust 用r#...#,Swift 用#"..."#且以\#引入转义,Java 则用反引号定界。 - 多行字符串(multiline string literals):C++/Rust/Java 用原始字符串兼作多行;Python 用
"""或''';Swift 用"""但内容不能与定界符同行;JavaScript 用反引号。
至于字符串插值(interpolation),例如"Hello, $planet.",明确不在本提案范围内。
提案核心:四个正交的字符串形态
提案的设计要点可以概括为:
- 单行字符串由一对
"定界:"hello"; - 多行(块)字符串由
'''引入并换行,以一行开头的'''结束,结束行的缩进会从前面所有行中剥离(当前实现的定界符;提案原文为"""); '''之后可跟随文件类型指示符(file type indicator),它只辅助工具理解字符串意图,不影响程序语义;- 转义序列以
\引入,支持最常见的 C/C++ 转义,移除八进制转义(\177)、\a、\b、\f、\v,并把\uNNNN/\U00NNNNNN统一为\u{NNNNNN}形式;块字符串中还允许\<newline>转义(不产生任何内容); - 原始字符串遵循 Swift 惯例:开定界符前缀一个或多个
#,闭定界符后缀相同数量的#,如#"foo\s*bar"#或#"foo"bar"#;在原始字符串中,\后跟匹配数量的#可重新引入转义,例如#"foo\#nbar"#包含一个换行符; - 与 C/C++ 不同,相邻字符串字面量不会隐式拼接。
"普通/原始"与"单行/多行"两个维度正交,组合出全部四种形态,这正是设计上刻意追求的结果——参见提案的 Rationale。
语法演进:从"""到'''
历史提案 p000199 原文使用"""作为块字符串定界符。但后续实践发现,[#]*"""会被用户想当然地认为是块字符串,实则存在歧义:例如"""abc"""会被解析为三个相邻字面量""、"abc"、"",而#"""#等价于"\""。为此提案 p001360 将语法明确为:[#]*"表示单行字符串,[#]*'''表示块字符串,同时禁止相邻字符串字面量("""abc"""因此成为非法写法)。当前词法实现中,'''是唯一合法的多行引入符,而"""会被 lexer 以MultiLineWithDoubleQuotes形态识别并报错:
error: use `'''` delimiters for a multi-line string literal, not `"""`该诊断的发射点在 toolchain/lex/string_literal.cpp,并由 toolchain/lex/testdata/multiline_string_literals.carbon 中的fail_quotes.carbon用例验证。
简单字符串字面量(非原始、单行)
简单字符串字面量由以下序列构成,并整体用"括起来:
- 除反斜杠、双引号和垂直空白以外的字符;
- 转义序列。
每个转义序列都会被替换为对应的字符序列或码元序列:
var String: lucius = "The strings, my lord, are false.";另外有几个重要约束:
- 字符串内只允许**空格(U+0020)**这种空白;制表符等其他水平空白被禁止,但会"解析进"字符串以支持错误恢复;
- 垂直空白不会成为单行字符串的一部分(单行字符串不能跨行);
- 无效转义(如
\z)同样为错误恢复目的解析进字符串; - 相邻字符串字面量被禁止——
"""abc"""会因三个相邻字面量""、"abc"、""而被拒绝并诊断。
这些规则在 toolchain/lex/testdata/string_literals.carbon 中有大量对应用例:fail_unknown_escape.carbon("\w"报unrecognized escape sequence)、fail_literal_tab_in_string.carbon("<TAB>"报whitespace other than plain space must be expressed with an escape sequence)、fail_unterminated.carbon等。
转义序列
下表是 Carbon 字符串字面量中完整可识别的转义序列:
| 转义 | 含义 |
|---|---|
\t | U+0009 字符制表符(CHARACTER TABULATION) |
\n | U+000A 换行(LINE FEED) |
\r | U+000D 回车(CARRIAGE RETURN) |
\" | U+0022 双引号(QUOTATION MARK,") |
\' | U+0027 撇号(APOSTROPHE,') |
\\ | U+005C 反斜杠(REVERSE SOLIDUS,\) |
\0 | 值为 0 的码元 |
\xHH | 值为 HH16的码元 |
\u{HHHH...} | Unicode 码点 U+HHHH... |
\<换行> | 不产生任何字符串内容(仅限块字符串) |
需要注意的细节:
- 十六进制字符必须大写:
H只能是0-9或A-F(大小写敏感),即\xAA合法而\xaa不合法;\x必须且只能跟恰好两位十六进制数字(与 Python 一致,与 C++ 的任意长度不同)。源码中HexadecimalEscapeMissingDigits诊断正是据此触发,见 string_literal.cpp。 - Unicode 码点用
\u{...}大括号形式(同 JavaScript/Rust/Swift),接受 1 到 8 个十六进制字符,可表达 016-D7FF16与 E00016-10FFFF16范围内的任意码点(即排除代理区)。超出0x10FFFF或落入代理区的码点会分别触发UnicodeEscapeTooLarge/UnicodeEscapeSurrogate错误,见 string_literal.cpp 及 string_literals.carbon 的测试。 \0后不得紧跟十进制数字;若需要在空字节后写数字,请用\x00:"foo\x00123"。\0跟数字会触发DecimalEscapeSequence诊断(string_literal.cpp),保留未来引入十进制转义的可能。
与 C++ 相比移除了哪些转义:\?(历史上用于 trigraph,已无用途);\ooo八进制转义(Carbon 不支持八进制字面量,但保留\0特例以服务 C 互操作);\uABCD与\U0010FFFF(统一改为\u{...}形式);\a(响铃)、\b(退格)、\v(垂直制表)、\f(换页)。需要这些字符时可分别用\x07、\x08、\x0B、\x0C表达。整体上,Carbon 的转义集合与 Swift、Rust 一致,额外多出 Swift 所没有的\xHH。
\<换行>转义:反斜杠后跟换行符(LF)是一个产生"无内容"的转义,属于实验性特性,只能出现在块字符串中。它按"先统一换行再处理转义"的顺序执行:\后跟水平空白再跟行终止符时,会删除该空白直到并包括行终止符。与 Rust 不同、与 Swift 类似:被转义换行的下一行前导空白不会被额外删除(仅删除与终止'''缩进匹配的部分)。示例如下:
// 该字符串不包含任何换行符。 var String: type_mismatch = ''' Shall I compare thee to a summer's day? Thou art \ more lovely and more temperate.\ '''; var String: trailing_whitespace = ''' This line ends in a space followed by a newline. \n\ This line starts with four spaces. ''';空白与无效转义的处理:以反斜杠开头但不匹配任何已知转义的字符序列是无效的。除空格(以及块字符串中的换行、可选的 CR+LF)外,其他空白字符一律禁止。其余所有字符(包括不可打印字符)原样保留。由于 Carbon 源文件必须是合法 Unicode 字符序列,非法 UTF-8 码元序列只能通过\x转义产生。
块字符串字面量(非原始、多行)
块字符串字面量以'''开头,随后是可选的文件类型指示符,再跟一个换行;它在下一个"第一个'不属于\'转义"的'''处结束。关闭的'''必须是该行的首个非空白字符。开行与闭行之间(不含两端)的各行称为内容行(content lines),内容行中不得出现不属于转义序列的\字符。
缩进剥离规则是块字符串最核心的语义:块字符串的缩进定义为闭行'''之前的水平空白序列。每个非空内容行都必须以该缩进开头,然后按如下步骤构造内容:
- 从每个非空内容行移除闭行缩进;
- 每行末尾的所有尾部空白(包括行终止符)替换为单个换行符(U+000A);
- 将处理后的各行拼接;
- 将每个转义序列替换为对应的字符序列或码元序列。
只含空白字符的内容行视为空行。因此块字符串默认带尾部换行(开头的换行不属于内容),第一个字符通常是首个内容行的首个非缩进字符:
var String: w = ''' This is a string literal. Its first character is 'T' and its last character is a newline character. It contains another newline between 'is' and 'a'. '''; // 该字符串字面量非法:'closing' 后的 ''' 终止了字面量,但它不在行首。 var String: invalid = ''' error: closing ''' is not on its own line. ''';缩进不匹配同样会被诊断。lexer 的CheckIndent会检查闭行后是否还有内容(ContentBeforeStringTerminator),ExpandEscapeSequencesAndRemoveIndent则逐行验证缩进一致性,不匹配时报MismatchedIndentInString(string_literal.cpp 与 multiline_string_literals.carbon 的fail_indent_mismatch.carbon用例)。
文件类型指示符(File Type Indicator)
文件类型指示符是引入行'''之后、去除周边空白后的文本,它不得包含'、#或";其后的//尾部注释属于普通注释,不进入指示符。指示符对 Carbon 编译器没有任何语义影响,但语言工具链(语法高亮、代码格式化器等)可依据它理解字符串内容的结构:
// 这是一个块字符串字面量。前两个字符是空格,最后一个字符是换行。 // 它的文件类型是 'c++'。 var String: starts_with_whitespace = '''c++ int x = 1; // This line starts with two spaces. int y = 2; // This line starts with two spaces. ''';指示符甚至可以携带超出文件类型本身的语义信息,例如指示代码格式化器对该代码块禁用格式化。提案留下的开放问题是:目前没有一套公认的指示符集合,未来最好在最佳实践指南中非正式地规定一组常见指示符,使各工具对指示符含义有共同理解(string_literal.cpp 的Introducer::Lex实现了指示符的裁剪与校验)。
原始字符串字面量
当字符串内容大量包含\与"时,可以通过给定界符加前缀N个#来定制。规则是:闭定界符必须紧跟N个#才被识别;转义序列也只有当\后跟N个#时才被识别;不满足这些条件的\、"、'''都不具有特殊含义。各级定界符对照如下:
| 开定界符 | 转义引入符 | 闭定界符 |
|---|---|---|
"/''' | \(如\n) | "/''' |
#"/#''' | \#(如\#n) | "#/'''# |
##"/##''' | \##(如\##n) | "##/'''## |
###"/###''' | \###(如\###n) | "###/'''### |
| ... | ... | ... |
举例如下:
var String: x = #''' This is the content of the string. The 'T' is the first character of the string. ''' <-- This is not the end of the string. '''#; // But the preceding line does end the string. // OK, final character is \ var String: y = #"Hello\"#; var String: z = ##"Raw strings #"nesting"#"##; var String: w = #"Tab is expressed as \t. Example: '\#t'"#;#"""的歧义消解:注意"单行原始字符串"与"多行原始字符串"都可以以#'''开头,两者的区分依据是同一行后面是否还有更多":若同一行稍后存在"加一个或多个#来终止,则是单行原始字符串;否则该行其余部分是文件类型指示符(不能含"或#)。当前语法下('''为块字符串),原始字符串词法完全由hash_level(开定界符前的#数量)驱动,见 string_literal.h。
原始字符串的一个重要优势是不损失任何功能:因为\#n这种形式在原始字符串中依然可以引入转义,维护中需要"在原始字符串里嵌入转义内容"时无需退回普通字符串。例如在原始块字符串中保留行尾空白,可以用更啰嗦的\#n\#终止符实现。
编码:UTF-8 与 8 位字节
字符串字面量求值结果是一串8 位字节序列。与 Carbon 源文件一样,字符串字面量以 UTF-8 编码,且本提案不提供请求其他编码的机制——如果确实需要其他编码,期望在编译期间从 UTF-8 转码。但这并不意味着字符串保证是合法 UTF-8:由于\xHH可以插入任意字节,字符串内容可能是任意字节序列。
这一决策目前是实验性的:如果出现直接以其他编码表达字符串字面量的充分动机,应重新审视。同时,随着库对字符串类型支持的演进,未来应考虑提供(也许作为默认)能保证字符串内容为合法 UTF-8 的字面量语法,让类型系统能够区分"合法 UTF-8"与"任意字符串";在这种字面量中,可考虑像 Rust 一样拒绝 HH 大于 7F16的\xHH转义。
源码实现佐证
- 词法入口与形态枚举:toolchain/lex/string_literal.h 定义了
StringLiteral::Kind:Char(字符字面量也经由字符串词法处理)、SingleLine、MultiLine、MultiLineWithDoubleQuotes(错误恢复用)。hash_level_记录#数量,content_needs_validation_标记是否需要转义展开(有转义或制表符时为真)。 - 转义展开与缩进剥离:toolchain/lex/string_literal.cpp 的
ExpandEscapeSequencesAndRemoveIndent是核心实现:逐行消费缩进、追加普通文本、处理换行与转义;它同时实现了"行尾空白替换为单个换行"与\<换行>转义逻辑。 - Unicode 展开:string_literal.cpp 的
ExpandUnicodeEscapeSequence校验码点范围(拒绝 >0x10FFFF 与代理区),再用 LLVM 的ConvertUTF32toUTF8转成 UTF-8 字节。 - 测试验证:toolchain/lex/testdata/string_literals.carbon 覆盖了无效转义、缺失十六进制数字、
\0后接数字、字符串内裸制表符、Unicode 代理区与越界码点等错误路径;toolchain/lex/testdata/multiline_string_literals.carbon 覆盖缩进不匹配、"""误用与引入行尾部注释。 - 类型层面:字符串字面量的类型为
Core.String,其前导类型定义位于 core/prelude/types/string.carbon,由ptr: Char*与size: i64构成;toolchain/check/testdata/primitives/string_literals.carbon 展示了字面量如何被转换为string_literal指令及长度/超长诊断。
备选方案与设计取舍
提案对若干关键决策进行了充分的替代方案比较,理解这些取舍有助于把握语法设计动机:
块字符串的替代:可以完全不做块字符串、用拼接构造多行文本,但那样更啰嗦且源码表达偏离程序员意图;也不宜像 C++ 那样用原始字符串兼作多行,因为那会把"是否识别转义"与"是否跨行"两个正交维度耦合。例如 C++ 的 Makefile 规则字符串:
std::string make_rule = "%s: %s\n\t$(CC) -c -o $@ $< $(CFLAGS)\n\n" "main:\n\t$(CC) %s -o %s\n";在 Carbon 中可以写成可读得多且制表符可见的块字符串:
var String: make_rule = '''make %s: %s \t$(CC) -c -o $@ $< $(CFLAGS) main: \t$(CC) %s -o %s ''';前导空白剥离方式:曾考虑用显式字符(如
|)标记要剥离的缩进量,这样在见到开'''后的第一行时即可确定缩进,但会增加词法复杂度并损害"把字符串内容拷贝到其他上下文"的能力,因此未采纳。尾部换行的取舍:Swift 不包含尾部换行,而 Carbon 选择包含尾部换行、不包含开头换行。这样在用多个块字符串拼接构造一个大字符串时,"每行都以换行结尾"的规则能让源码层级与结果字符串对齐最佳——示例可见原提案中分段打印 C++ 类的
Run()函数。转义序列的取舍:不支持八进制转义(与不支持八进制数字字面量保持一致,且调查显示许多 C++ 程序员不知道
\123是八进制);\x固定两位十六进制(C++ 的任意长度需要显式终止机制,属"不必要的发明");不引入 Python 的\N{unicode character name};曾讨论过\e(U+001C ESCAPE,常用于硬编码 ANSI 终端序列如"\e[32mgreen text\e[0m"),提案未明确拒绝,但认为未来一个终端操作库设施可能比该转义更有价值;\u{...}采用 Swift/Rust 的单一大括号形式而非 JavaScript 的\uNNNN短形式,因为预期常规可打印且 NFC 归一化的 Unicode 字符会直接写在源码里,显式\u转义主要用于测试数据或方向性标记等特殊字符。原始字符串的取舍:采用 Swift 式
#方案而非 C++ 式自定义定界符(嵌套一两层时#"..."#、##"..."##足够,无需程序员做任意选择);在@、#、$、)、]、}、\、.等可用字符中选#(\与转义冲突、闭括号可能用于未来语法、.太像设计符);不采用 Rust 式r前缀 + 禁转义方案,因为 Swift 式是普通字符串的推广(非原始只是#数量为零的特例),且保留转义能力意味着维护时不会被迫放弃原始字符串。已知弱点(如"\\################"这类极端字符串难以用原始字符串表达)可通过"\后跟 N+1 个以上#原样保留"的规则修补。内部空白:字符串中不允许裸制表符,鼓励在可用处用
\t(原始字符串中则用更啰嗦的\#t),以保护程序可读性。
总结
Carbon 的字符串字面量设计以"可读、可工具化、表达自然"为目标,用普通/原始与单行/块两组正交维度覆盖几乎所有字符串内容场景:块字符串的缩进剥离让多行文本融入代码缩进结构,#级联定界让含大量\与"的文本免于转义地狱,\#n机制又保证原始字符串不损失转义能力,\xHH保留了非 UTF-8 字节序列的表达通道,文件类型指示符则为语法高亮、格式化乃至静态分析工具提供了嵌入式代码的识别线索。其整体哲学是避免不必要的发明,沿袭 Rust 与 Swift 的成熟做法——这正是它在转义集合、\u{...}形式与原始字符串语法上与之高度一致的原因。如需查阅完整规范,请以词法约定设计文档为准,它基于本提案并在语法细节上持续演进。
【免费下载链接】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),仅供参考