- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
导读
本文围绕 RFC 仓库中的 text/0879-small-base-lexing.md(RFC 879)展开,深入讲解 Rust 编译器词法层(lexer)对二进制(0b)与八进制(0o)数字字面量的词法规则设计:把这两种"小进制"字面量像十进制一样连续读取全部数字字符,而不是遇到非法数字就立刻截断切分 token。读完本文,你将理解为什么0b0123在宏展开中会静默产生"错误的结果"、新词法规则如何从根本上消除这类问题,以及该设计与 Rust 字面量后缀、保留前缀等周边词法机制之间的关联。
一、问题背景:Rust 词法层如何切分数字字面量
Rust 的源码处理分为词法(lexing/tokenizing)与语法(parsing)两个阶段。词法层负责把字符流切成 token,数字字面量则按四种进制分别识别。RFC 879 所针对的,正是词法层对二进制与八进制字面量"只认本进制合法数字"这一行为。
在同期文档 text/0090-lexical-syntax-simplification.md 中,作者用 ANTLR 文法完整描述了当时的词法语法,其中整数字面量的规则原文如下:
LIT_INTEGER : [0-9][0-9_]* INT_SUFFIX? | '0b' [01][01_]* INT_SUFFIX? | '0o' [0-7][0-7_]* INT_SUFFIX? | '0x' [0-9a-fA-F][0-9a-fA-F_]* INT_SUFFIX? ;可以看到:二进制字面量的数字部分被限定为[01],八进制被限定为[0-7],十六进制则允许[0-9a-fA-F]。也就是说,词法器一遇到不属于该进制的数字(二进制中的 2–9、八进制中的 8–9)就会停止读取当前字面量,并立刻开始切分下一个 token。
二、动机:非法数字截断导致的两个实际问题
RFC 879 指出,上述"遇到非法数字即截断"的行为会带来两个层面的问题。
1. 普通代码中出现令人费解的错误信息
以0b0123为例:词法器读到0b01后遇到数字2——它不是合法的二进制数字,于是立即停止,把0b01当作一个 token,把紧随其后的23当作另一个 token。在普通表达式上下文里,两个 token 相邻是不合法的,于是编译器给出如下与问题本源毫无关联的报错:
<anon>:2:9: 2:11 error: expected one of `.`, `;`, `}`, or an operator, found `23` <anon>:2 0b0123 ^~错误信息说的是"在23前面期望出现.、;、}或运算符",完全没有提示用户真正的错误在于0b0123中混入了非二进制数字2、3。显然,这与用户的实际意图(写出一个完整的二进制字面量)相去甚远。
2. 宏中可合法出现的"相邻 token"导致静默错误结果
普通代码中两个 token 直接相邻必然报错,但在macro_rules!中,使用tt(token tree,token 树)非终结符可以合法地捕获任意相邻的 token 序列。于是词法层的"截断切分"就会在宏展开时被放大成错误行为。
RFC 给出了一个极具说服力的示例:
macro_rules! expr { ($e: expr) => { $e } } macro_rules! add { ($($token: tt)*) => { 0 $(+ expr!($token))* } } fn main() { println!("{}", add!(0b0123)); }这段代码的输出是24。展开过程是:0b0123在词法层被切成0b01与23两个 token,add!宏将其展开为0 + 0b01 + 23,即0 + 1 + 23 = 24。用户写下的0b0123本意是想表达某个二进制数(在修复后的语义下,它应当是一个词法错误),但程序却"正常"编译并打印出了 24——一个与用户意图完全无关、且没有任何警告的静默错误结果。
RFC 强调:普通代码中的问题或许还可以通过在词法器里特判该模式、定制专属错误信息来缓解,但宏场景无法用这种方式解决,因为tt捕获本身就允许 token 相邻存在。这正是必须从词法规则层面下手的原因。
三、详细设计:小进制字面量按"十进制字符集"连续词法化
1. 新词法语法
RFC 879 的核心改动非常简洁:词法层的字面量语法从"各进制只认自己的数字字符集"改为统一按十进制数字字符集[0-9]读取:
(0b[0-9]+ | 0o[0-9]+ | [0-9]+ | 0x[0-9a-fA-F]+) suffix即二进制由原来的[01]放宽为[0-9],八进制由原来的[0-7]放宽为[0-9],十六进制保持[0-9a-fA-F]不变。整个数字字面量作为一个整体被读取,直到遇到非数字字符(或命中后缀)为止。
2. 错误仍然必须存在:非法数字是词法错误
放宽字符集并不意味着0b2这样的写法变得合法。RFC 明确规定:以0b或0o开头的数字字面量中若出现非法数字,在词法层就是错误(always an error in the lexer)。非法性从"token 切分点"转移为"整体字面量的内容校验",报错时机反而更清晰。
特别值得注意的是,RFC 用如下示例强调这种错误不因 token 未被使用而豁免:
macro_rules! ignore { ($($_t: tt)*) => { {} } } ignore!(0b0123)即使宏ignore!匹配到 token 后将其完全丢弃、什么也不做,0b0123依然是一个词法错误。也就是说,非法数字的判定发生在词法层,早于宏展开与语义分析,任何宏都无法"吞掉"这类非法字面量。
3. 预期的错误信息形态
RFC 期望对两个场景(普通代码与宏)都给出明确指向的报错,其设想的信息格式如下:
error: found invalid digit `2` in binary literal 0b0123 ^错误信息明确说出"在二进制字面量中发现非法数字2",并用插入符号精确指向出错的位置。相比旧的expected one of ...报错,这种信息能直接引导用户发现0b0123中混入了2、3。
4. 想要两个 token?请显式写出空格
对于确实希望把数字拆成两个 token 的代码,RFC 给出明确约定:用空格隔开即可,例如0b01 23。这既写法简单,也更能表达代码的真实意图——毕竟"相邻的两个数字 token"和"一个数字字面量"在语义上是截然不同的。
四、与相邻词法机制的关联:字面量后缀与保留前缀
数字字面量的词法行为并非孤立设计。RFC 879 的语法中末尾的suffix一词,指向 Rust 字面量后缀机制(1u8、0x10u32等)。理解周边文档有助于把握该设计的完整脉络。
1. 字面量后缀的"整体 token"化:RFC 463
在 text/0463-future-proof-literal-suffixes.md(RFC 463)中,Rust 将字面量后紧跟的标识符也并入同一 token(例如"foo"bar、1baz被视为一个整体 token,而不是"foo"与bar两个 token),目的同样是为未来扩展保留空间、避免宏中相邻 token 带来的歧义。该 RFC 的示例0b11u25即同时涉及小进制数字与后缀:
0b11u25 0x26i27 28.29e30f31可见,小进制字面量与后缀机制共同构成了"数字字面量整体化"的演进方向:先保证数字部分按完整字符集读取(RFC 879),再保证后缀作为字面量的一部分被吞入(RFC 463),二者一前一后,把曾经模糊的 token 边界问题彻底消除。
2. 保留前缀:2021 edition 对词法空间的事先封锁
更晚的 text/3101-reserved_prefixes.md(RFC 3101)从 2021 edition 起保留了ident#foo、ident"foo"、ident'f'、ident#123这类语法形式,明确禁止未赋予含义的前缀直接附着在字面量上。该文档的动机部分特别提到"与字面量后缀机制的对称性"以及"为未来新增字面量前缀保留设计空间"。这与 RFC 879 共享同一个核心理念:词法层的 token 边界直接决定宏与未来语言扩展的可用空间,必须提前设计得保守而清晰。
五、代价与权衡:Rust 为何"与众不同"
RFC 879 坦率地列出了该设计的代价:
- 它为 Rust 引入了一个略显特殊的规则——
0b0123这种写法在 Rust 中是一个 token 且必报错,这在语法设计上是一个带有"独特个性"的特例。 - 之所以大多数语言不必如此费心,是因为多数语言不直接暴露词法层给用户/宏系统。在那些语言里,
0b1234到底是一个 token 还是两个 token 无法从外部区分,因为它无论如何都是错误。而 Rust 的macro_rules!允许以tt直接捕获 token 序列,词法边界因此成为可被用户观察、可被宏消费的语言行为,必须显式地、保守地定义。
这一权衡的深层逻辑是:词法边界的可观察性越高,语言设计者就越有义务让边界"无歧义"。Rust 选择了"非法数字仍整体成 token 并报错"这条路,从而保证任何宏都无法借助词法截断行为产生意外结果。
六、备选方案与未决问题
RFC 依次评估了如下替代设计:
- 什么都不做(Don't do it):维持现状,但前述宏场景的静默错误就无法根除。
- 把
0b123理解为0b1加后缀23:即把紧跟的非法数字当作字面量后缀,是否报错取决于"后缀23是否合法"。RFC 指出,若要统一处理这种"字面量后紧跟内容"的情形,就需要把"foo"123、'a'123也一并词法化为单个 token——这与 RFC 463 的思路殊途同归,作者也承认"这可能本来就是个好主意"。
关于未决问题,RFC 879 给出的结论是None(无):设计在提出时已自洽,无需留待后续讨论。
七、总结:一条"限制语言"的词法规则
RFC 879 的头部元数据中有一句值得玩味的话——该特性的性质被标注为stable, it only restricts the language(稳定特性,它只是限制语言)。这句话精准概括了这次改动的本质:它没有引入任何新语法能力,而是收紧了合法程序的集合,把"曾经合法(但结果荒谬)"的宏写法变成"明确报错"的非法写法,用一次有限的破坏换取长期的可预期性。
最终形成的词法约定可以浓缩为三条:
0b/0o开头的字面量按[0-9]完整读取,非法数字不再充当 token 切分点;- 非法数字是词法级错误,任何宏(哪怕直接丢弃 token)都无法绕过;
- 需要多个 token 的写法请显式写空格(如
0b01 23),意图更清晰。
对于 Rust 编译器开发者与宏作者而言,这条规则意味着:词法边界是宏语义的基石,设计时宁可保守报错,也不要让截断行为悄悄改写程序含义。
- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
相关推荐
Traceur Compiler数值字面量处理:二进制与八进制转译
Traceur Compiler数值字面量处理:二进制与八进制转译 JavaScript的数值表示法在ES6中引入了二进制( 0b / 0B 前缀)和八进制(
编译器编程语言开发工具core-js 的 ECMAScript Number 特性解析:二进制/八进制字面量解析与全套数字 API 的 Polyfill 实现
core js 的 ECMAScript Number 特性解析:二进制/八进制字面量解析与全套数字 API 的 Polyfill 实现 本篇技术指南以 cor
标准库ES6二进制与八进制字面量:基于gh_mirrors/es/es6features项目
ES6二进制与八进制字面量:基于gh_mirrors/es/es6features项目 在JavaScript开发中,你是否还在为处理二进制和八进制数值时的繁琐
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考