
Carbon 语言运算符 Token 设计解析固定符号集、Max Munch 词法与空白决定的中缀/前缀/后缀【免费下载链接】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 仓库中的设计提案 proposals/p000601-operator-tokens.md系统讲解 Carbon 运算符 token 的设计决策语言为何采用固定运算符 token 集、符号 token 如何通过max munch 最长匹配规则进行词法切分、以及如何借助运算符两侧空白同时区分中缀binary、前缀prefix与后缀postfix三种运算符形式。读完本文你将理解 Carbon 词法层运算符设计的完整脉络并能对照 token_kind.def、lex.cpp 与 tokenized_buffer.h 中的真实实现印证这些规则。问题背景运算符 token 从何而来任何编程语言都需要一组 token 来表示运算符。围绕运算符 token 如何生成业界存在两条截然不同的技术路线固定 token 集路线语言规范预先定义好一组运算符 token开发者不能自行发明新的运算符符号。例如C有一组固定的运算符 token且and、or等关键字形式的运算符是、||等符号运算符的词法同义词Rust同样在语言层面预定义运算符集合。可扩展运算符路线语言提供可扩展的运算符定义规则允许开发者注册不属于基础语言的运算符。例如Swift与Haskell都允许自定义运算符。除了是否可扩展这一维度还存在词法切分策略的差异。对于一段连续的符号字符序列主流做法有三种每次词法切分都取已知的最长运算符在每一步词法切分时从当前位置的剩余字符序列中匹配出规范中定义的最长运算符。例如在 C 中a b是 3 个 tokena、、b而a b是 4 个 tokena、、、b因为语言中存在、、却不存在。这种方法通常被称为max munch。每次词法切分都取最长的符号字符序列作为运算符若该序列并非已定义的运算符则程序直接非法。例如在一个类 C 语言中a b会因不是合法运算符而报错而不是被解释为a (b)。借助语义信息切分依据操作数类型等语义信息决定如何把一串符号字符拆分成一个或多个运算符。提案核心固定 token 集 两种 token 种类提案给出的答案是Carbon 使用由语言规范定义的一组固定 token 来表示运算符开发者不能定义新 token 来代表新运算符运算符重载等设施属于提案范围之外的内容。这组 token 分为两种符号 tokenSymbolic tokens由一个或多个符号字符组成不含任何可在标识符中出现的字符、不含引号字符、不含空白。关键字 tokenKeywords遵循单词的词法规则。符号 token 采用max munch 规则词法切分每次词法切分时若当前位置存在规范定义的符号 token 起始则切出最长的那一个。一个重要的澄清并非语法中所有符号 token 都充当运算符。例如(与)用于界定多种语法产生式.也不宜视为运算符——因为它的右操作数并不是表达式。两类 token 的分工不是同义改写提案特别强调两种 token 面向不同的用途而不是同一功能的两种拼写符号 token用于广为人知的运算符如数学运算符、*、等。这类符号运算符通常预期对某些用户自定义类型也有意义是未来实现运算符重载时的候选对象。关键字 token用于以下场景执行流程控制的运算符如and、or、throw、yield以及与它们紧密相关的not。这些运算符的行为超越了求值操作数并计算值必须从其他运算符中突显出来罕见、不值得占用有限符号 token 预算的运算符如可能的 xor 或循环移位优先级极低、以及部分优先级极高的运算符没有约定俗成符号、也不打算发明符号的专用运算符例如as。提案明确声明本节的示例运算符仅用于解释两类 token 的设计动机并不在本提案中实际引入这些运算符。源码印证在 toolchain/lex/token_kind.def 中可以看到and、or、not、as等都被定义为CARBON_KEYWORD_TOKEN关键字 token而、*、、等被定义为CARBON_SYMBOL_TOKEN符号 token与提案的分类完全一致。符号 token 初始清单提案给出的初始符号 token 清单如下(){}[],.;:*-该清单仅覆盖当时已批准的语法产生式并会随着后续语言提案引入更多符号 token 而持续扩充。实现中的演进从 15 个到一整套符号表对照当前仓库的 toolchain/lex/token_kind.def可以看到这张初始清单已经大幅扩展。文件以 X-macro 的方式统一声明所有 token并明确要求符号 token 按拼写从长到短排序注释直接写道symbols need to be ordered from longest to shortest to effectively provide max-munch lexing见 token_kind.def。当前实现包含三字符符号、、、-?两字符符号、^、:、:?、、、!、、、、、、-、-、-、--、%、|、、、/、*、~单字符符号、、\、^、:、、!、、、-、%、.、|、、?、/、*、~成对分组符号(/)、{/}、[/]通过CARBON_OPENING_GROUP_SYMBOL_TOKEN与CARBON_CLOSING_GROUP_SYMBOL_TOKEN建立开闭关联单字符终结/分隔符,与;被定义为CARBON_ONE_CHAR_SYMBOL_TOKEN——这类符号构造上恰好是一个字符无法再与其他字符组合成新符号因此无需参与 max-munch 匹配见 token_kind.def。这种 X-macro 设计让新增符号 token只需在token_kind.def中加一行声明编译期即可自动生成分发表恰好呼应提案中该清单应随提案持续扩展的预期。Max munch 在词法器中的真实实现lex.cpp 中的LexSymbolToken是多字符符号 token 切分的核心实现。它借助 LLVM 的StringSwitch从当前位置的子串开始匹配并针对token_kind.def中的每个CARBON_SYMBOL_TOKEN生成一个.StartsWith(Spelling, TokenKind::Name)分支TokenKind kind llvm::StringSwitchTokenKind(source_text.substr(position)) #define CARBON_SYMBOL_TOKEN(Name, Spelling) \ .StartsWith(Spelling, TokenKind::Name) // ... #include toolchain/lex/token_kind.def .Default(TokenKind::Error); if (kind TokenKind::Error) { return LexError(source_text, position); } TokenIndex token LexToken(kind, position); position kind.fixed_spelling().size(); return token;正是先声明三字符、再声明两字符、最后声明单字符的排序保证了对-与-?、与、这类短拼写是长拼写前缀的 token 总能优先匹配更长的那一个——这就是提案中 max munch 规则的落地点。单字符符号、分组符号则由LexOneCharSymbolToken、LexOpeningSymbolToken、LexClosingSymbolToken等专用函数经分发表单独处理见 lex.cpp其中开闭符号还会维护open_groups_栈用于括号配对与错误恢复。空白规则同一符号 token 如何兼作中缀/前缀/后缀提案最核心、也最独特的设计是用运算符两侧空白的有无来确定运算符的fixity中置性与人类读者的阅读直觉保持一致。以*为例issue #523 已决定*未来要同时支持前缀、中缀、后缀三种用法表达式a * - b存在两种合法解析a * (- b)a乘以b的取负(a *) - b从指针类型a *减去b。仅靠 token 本身无法区分因此提案以空白为判据a * -b采用第一种解释*两侧有空白 → 中缀-与操作数b之间无空白 → 一元前缀a* - b采用第二种解释*右侧有空白、左侧无空白 → 后缀-两侧有空白 → 中缀其余组合a*-b、a *- b、a* -b、a * - b、a*- b、a *-b一律作为错误拒绝。空白规则的正式表述完整规则如下一元运算符与其操作数之间不得有空白二元运算符两侧的空白必须一致要么两侧都有空白要么两侧都无空白若二元运算符两侧均无空白则运算符之前的 token 必须是标识符、字面量或任意类型的闭括号如)、]、}运算符之后的 token 必须是标识符、字面量或任意类型的开括号如(、[、{。规则 3 的意义在于允许2*x*x 3*x 1这类用无空白表达更高优先级的写法。判定操作数开始/结束的 token 集合被明确定义为标识符如x*x y*y字面量如3*x 4*y或foos任意类型的括号方向背对运算符如f()*(n 3)或args[3]*{.real4, .imag1}。五种 token 变体从 token 形成的角度看空白规则使每个符号 token 拥有四种变体variants变体空白分布可充当的角色二元变体binary两侧均有空白二元中缀运算符二元变体binary两侧均无空白且前一 token 为标识符/字面量/闭括号、后一 token 为标识符/字面量/(二元中缀运算符一元变体unary两侧均无空白且不满足上一行条件前缀或后缀运算符前缀变体prefix仅在左侧有空白前缀运算符后缀变体postfix仅在右侧有空白后缀运算符使用规则为在非运算符上下文中任何变体都可接受在运算符上下文中只有二元变体可作二元运算符只有前缀或一元变体可作前缀运算符只有后缀或一元变体可作后缀运算符。错误恢复的边界情况从错误恢复角度该规则要求没有表达式上下文可以以看似操作数结束的 token 开头也没有表达式上下文可以以看似操作数开始的 token 结尾。一个已知例外出现在函数定义中fn F(p: Int *) - Int * { return p; }这里的两个Int *都是错误的*后直接跟闭括号或{无法构成合法的一元后缀用法也不满足两侧无空白二元变体的条件。第一个Int *容易检测与诊断第二个则更具挑战性——前提是{...}本身是合法的表达式形式。提案预期可以容易地区分以{开始的代码块与以{开始的表达式{}除外。而{}作为带返回类型函数体并不合理因此错放空白 {}组合导致的错误预计罕见剩余情况也能良好恢复。实验性声明与实现状态提案明确标注禁止一元运算符与其操作数之间存在空白这一选择是实验性的。空白规则当时已在两个层面落地Carbon 工具链PR #576通过在 token 中记录其后是否带尾随空白对所有运算符实施该规则可执行语义commit04d3a885ae01a779aadb19f51ec7a5a12ffe295c针对*运算符按上文描述形成四种不同的 token 变体。源码印证当前仓库中空白信息由 tokenized_buffer.h 暴露两个查询接口——HasLeadingWhitespace(TokenIndex)与HasTrailingWhitespace(TokenIndex)。其实现tokenized_buffer.h读取TokenInfo中的has_leading_space()标志某 token 是否带尾随空白实际等价于检查其后继 token 是否带前导空白。也就是说每个 token 只需记录自己是否紧跟空白解析器即可据此推断运算符两侧的空白分布进而判定其 fixity 变体。设计理由与 Carbon 语言目标的对齐提案从 Carbon 的三项核心目标出发给出理由软件与语言演进Software and language evolution不允许用户自定义运算符降低了未来语言新增运算符与既有用法冲突的可能性。虽然 max munch 规则意味着新增运算符可能改变既有代码的解释但由于运算符集合预先可知这类问题易于检测和解决。易于阅读、理解和编写的代码固定运算符集意味着开发者无需理解无界、可扩展的运算符及其优先级规则不适合用知名符号表达的功能倾向于以具名操作而非符号暴露提升了不熟悉特定代码库的开发者的可读性要求运算符周围空白使用一致减少混淆性格式化的可能允许二元运算符两侧同时有空白或两侧同时无空白使2*x*x 3*x 1这类通过省略空白提升可读性的写法获得语言官方认可格式化工具因而可以预期保留用户的空白选择选择切分最长已知符号 token而非切分最长符号字符序列使x -*p;这类连续前缀/后缀运算符的表达更易书写。与既有 C 代码的互操作与迁移固定运算符集使 Carbon 运算符到 C 运算符的映射更简单——无需处理把任意自定义运算符映射为 C 形式的需求固定运算符集 max munch 规则与 C 采用的方案一致对 C 开发者而言非常熟悉空白规则允许*同时承担乘法、解引用、指针类型构成三种角色与 C 相同同时让 Carbon 能够把类型表达式当作表达式处理。备选方案回顾提案还讨论了三种被否定的备选方案可作为理解本设计权衡的参照备选一切分最长符号字符序列而非最长已知运算符优点新增运算符无需改动词法规则若未知运算符一律拒绝新增运算符也不会改变既有合法代码的含义。缺点连续的前缀/后缀运算符将被迫加括号或用空白分隔。例如Int**会被切分为Int 单个**token**p会被切分为单个**p当不存在**运算符时。虽然可以定义**、***……为运算符但会给语言规则增加复杂性与不一致性。备选二可扩展运算符集优点提升表达力尤其是嵌入式领域特定语言DSL。缺点损害可读性至少对不熟悉相关代码库的人自定义运算符可能与新引入的标准运算符冲突损害语言演进能力虽可通过为自定义运算符提供独立词法语法来降低风险要么采用最长符号字符序列切分有备选一的缺点要么引入更复杂的切分规则如依据当前作用域内可用的运算符重载来切分增加复杂度。备选三不同的空白限制要求二元运算符后跟[或{时必须加空白issue #520 有完整讨论与 leads 决策。对fn F() - Int*{ return Null; }与var n: Int pointer_to_array^[i];这类例子该规则可以形成一元运算符而非二元运算符更符合开发者预期。优点为新增产生数组的后缀^解引用运算符或类似后缀运算符留出空间不会对指向数组的指针造成意外也允许函数体的{前一致地省略空白。缺点规则更复杂且不对称必须允许闭方括号出现在无空白二元运算符之前以支持arr[i]*3与以[或{开头的表达式形式相互干扰例如Time.Now(){.seconds 3}或names[Lrrr]。小结一份少即是多的词法契约p000601提案确立的运算符 token 设计可以概括为三条契约固定符号集运算符 token 由语言规范唯一确定符号 token 用 max munch 最长匹配切分关键字 token 走单词词法两者分工明确、不可互相替代空白即语义运算符两侧空白的有无决定其中缀/前缀/后缀身份为*这类一符多用的运算符提供了简单、无歧义、利于错误恢复的解析依据演进受控任何新运算符都必须经由语言提案进入 token_kind.def 的符号表同时由于集合固定且顺序已知新增 token 对既有代码的影响可被预先评估。这份设计既吸收了 C 固定运算符与 max munch 的成熟经验又以空白对称性换取了远超 C 的表达式书写自由度是理解 Carbon 词法与语法层后续设计如 p001191-bitwise-and-shift-operators.md、p000601 之后的运算符扩展提案的重要基石。若希望进一步研究实现细节建议从 toolchain/lex/lex.cpp 的LexSymbolToken与 toolchain/lex/tokenized_buffer.h 的空白查询接口入手配合 toolchain/lex/tokenized_buffer_test.cpp 中的测试用例验证各类空白组合的切分结果。【免费下载链接】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),仅供参考