Rust RFC 0879 解读:二进制与八进制数字字面量的词法分析重构
文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载导读本文围绕 RFC 仓库中的 text/0879-small-base-lexing.mdRFC 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!中使用tttoken treetoken 树非终结符可以合法地捕获任意相邻的 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两个 tokenadd!宏将其展开为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.mdRFC 463中Rust 将字面量后紧跟的标识符也并入同一 token例如foobar、1baz被视为一个整体 token而不是foo与bar两个 token目的同样是为未来扩展保留空间、避免宏中相邻 token 带来的歧义。该 RFC 的示例0b11u25即同时涉及小进制数字与后缀0b11u25 0x26i27 28.29e30f31可见小进制字面量与后缀机制共同构成了数字字面量整体化的演进方向先保证数字部分按完整字符集读取RFC 879再保证后缀作为字面量的一部分被吞入RFC 463二者一前一后把曾经模糊的 token 边界问题彻底消除。2. 保留前缀2021 edition 对词法空间的事先封锁更晚的 text/3101-reserved_prefixes.mdRFC 3101从 2021 edition 起保留了ident#foo、identfoo、identf、ident#123这类语法形式明确禁止未赋予含义的前缀直接附着在字面量上。该文档的动机部分特别提到与字面量后缀机制的对称性以及为未来新增字面量前缀保留设计空间。这与 RFC 879 共享同一个核心理念词法层的 token 边界直接决定宏与未来语言扩展的可用空间必须提前设计得保守而清晰。五、代价与权衡Rust 为何与众不同RFC 879 坦率地列出了该设计的代价它为 Rust 引入了一个略显特殊的规则——0b0123这种写法在 Rust 中是一个 token 且必报错这在语法设计上是一个带有独特个性的特例。之所以大多数语言不必如此费心是因为多数语言不直接暴露词法层给用户/宏系统。在那些语言里0b1234到底是一个 token 还是两个 token 无法从外部区分因为它无论如何都是错误。而 Rust 的macro_rules!允许以tt直接捕获 token 序列词法边界因此成为可被用户观察、可被宏消费的语言行为必须显式地、保守地定义。这一权衡的深层逻辑是词法边界的可观察性越高语言设计者就越有义务让边界无歧义。Rust 选择了非法数字仍整体成 token 并报错这条路从而保证任何宏都无法借助词法截断行为产生意外结果。六、备选方案与未决问题RFC 依次评估了如下替代设计什么都不做Dont do it维持现状但前述宏场景的静默错误就无法根除。把0b123理解为0b1加后缀23即把紧跟的非法数字当作字面量后缀是否报错取决于后缀23是否合法。RFC 指出若要统一处理这种字面量后紧跟内容的情形就需要把foo123、a123也一并词法化为单个 token——这与 RFC 463 的思路殊途同归作者也承认这可能本来就是个好主意。关于未决问题RFC 879 给出的结论是None无设计在提出时已自洽无需留待后续讨论。七、总结一条限制语言的词法规则RFC 879 的头部元数据中有一句值得玩味的话——该特性的性质被标注为stable, it only restricts the language稳定特性它只是限制语言。这句话精准概括了这次改动的本质它没有引入任何新语法能力而是收紧了合法程序的集合把曾经合法但结果荒谬的宏写法变成明确报错的非法写法用一次有限的破坏换取长期的可预期性。最终形成的词法约定可以浓缩为三条0b/0o开头的字面量按[0-9]完整读取非法数字不再充当 token 切分点非法数字是词法级错误任何宏哪怕直接丢弃 token都无法绕过需要多个 token 的写法请显式写空格如0b01 23意图更清晰。对于 Rust 编译器开发者与宏作者而言这条规则意味着词法边界是宏语义的基石设计时宁可保守报错也不要让截断行为悄悄改写程序含义。赞分享文档【免费下载链接】rfcsRFCs for changes to Rust项目地址https://gitcode.com/gh_mirrors/rf/rfcs点击查看免费下载相关推荐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开发中你是否还在为处理二进制和八进制数值时的繁琐上一篇LMFlow NLL 自动评测框架详解LLM 对话、常识与指令跟随能力的低成本 Benchmark 实践下一篇Celery 并发执行池使用 gevent 实现高并发任务处理实战指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考