从快照测试看 Roc 十六进制整数字面量的编译流水线:词法、规范化与 Dec 类型推断
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
导读
本文以 Roc 编译器仓库中的快照测试 test/snapshots/can_hex_integer.md 为线索,完整还原一个十六进制整数字面量0xFF从源码到类型推断结束的整条编译流水线,并对照src/parse、src/canonicalize、src/types等目录下的真实实现逐一解读。读完本文,你将掌握 Roc 数字字面量的进制前缀规则、0x字面量的解析与规范化原理、为何未标注类型的数字字面量最终默认推断为Dec,以及如何阅读与运行仓库内的快照测试来验证编译器行为。
快照测试:编译器行为的逐阶段"留证"
Roc 仓库的test/snapshots/目录下存放着一批快照测试文件。按照 test/snapshots/README.md 的说明,这些快照通过捕获特定 Roc 代码示例在编译流水线每个阶段的输出来验证编译器行为,覆盖分词(tokenization)、解析(parsing)、规范化(canonicalization)、类型检查(type checking)等阶段;每个快照文件都记录了预期的输出,当编译器行为意外变化时能帮助开发者及时检测回归。
can_hex_integer.md正是其中之一,它的META头声明了该用例的意图:
description=Hexadecimal integer literal type inference type=snippet即:十六进制整数字面量的类型推断,快照类型为snippet(代码片段)。整个文件就是对一个极简程序的完整编译过程记录。快照的各段落(SOURCE、EXPECTED、PROBLEMS、TOKENS、PARSE、FORMATTED、CANONICALIZE、TYPES)恰好对应编译器的不同阶段,下文逐一拆解。
源程序与预期结果
快照的SOURCE段是被编译的 Roc 源码,只有一行:
x = 0xFF这是一个顶层声明:将十六进制字面量0xFF绑定给变量x。0xFF在十六进制下是15×16 + 15 = 255。
EXPECTED与PROBLEMS两段都输出NIL,含义是:编译全程没有产生任何诊断报告。根据 test/snapshots/README.md,PROBLEMS段保存的是各reporting.Report的规范 S 表达式序列化(实现在 src/reporting/report_sexpr.zig),NIL表示编译无报告。这印证了一个事实:0xFF是合法、无歧义、类型可推断的字面量,编译器对它零告警地通过了所有阶段。
词法分析(TOKENS):0xFF如何变成一个Int记号
快照的TOKENS段给出了分词结果:
LowerIdent,OpAssign,Int, EndOfFile,x被识别为LowerIdent(小写标识符);=被识别为OpAssign(赋值操作符);0xFF被整体识别为Int(整数记号);- 末尾是
EndOfFile。
值得注意:0xFF在词法阶段就是一个完整的Inttoken,而不是0x前缀加FF的组合。这得益于分词器对数字的整段吞入策略。在 src/parse/tokenize.zig 的chompNumberSuffix等逻辑中,tokenizer 会持续吞入数字字符(含后续字母、数字、下划线等标识符字符),直到遇到不属于数字的字符为止。
语法解析(PARSE):进入表达式树
PARSE段以 Clojure 风格 S 表达式展示了解析出的语法树:
(file (type-mod) (statements (s-decl (p-ident (raw "x")) (e-int (raw "0xFF")))))- 顶层是
file,含一个空的type-mod(类型模块); statements中有一个s-decl(语句级声明);- 声明左侧是
p-ident (raw "x")(模式:标识符),右侧是e-int (raw "0xFF")(表达式:整数,原文"0xFF")。
e-int对应解析器中的整数表达式节点。在 src/parse/Parser.zig 中,当解析器遇到Inttoken 时会调用addNumericLiteral记录该数字字面量的原文与类别(int或frac),并构造表达式节点。同时,解析器会检查 token 是否携带已废弃的紧凑类型后缀(如u64、dec),见 src/parse/NumericLiteral.zig 的DeprecatedSuffix枚举与deprecatedSuffixFromText。0xFF没有此类后缀,因此不产生任何告警。
格式化(FORMATTED):源码已符合规范
NO CHANGEFORMATTED段输出NO CHANGE,表示这段源码经过 Roc 的格式化器处理后没有任何改动——x = 0xFF本身就是规范的书写形式。这也间接说明0xFF这种十六进制写法是官方推荐的、会被格式化器原样保留的写法。
规范化(CANONICALIZE):0xFF变为e-num (value "255")
规范化阶段是理解整个用例的核心。CANONICALIZE段展示了从语法树到编译器内部表示(CIR,Canonical IR)的转换:
(can-ir (d-let (p-assign (ident "x")) (e-num (value "255"))))- 声明变为
d-let,绑定模式是p-assign (ident "x"); - 关键变化:
e-int (raw "0xFF")变成了e-num (value "255")——十六进制字面量在规范化时被换算成十进制值 255,存入 CIR 的e_num节点。
e_num节点的定义见 src/canonicalize/Expression.zig,其注释明确列出了支持的进制写法:
/// 0xFF # Hexadecimal integer /// 0o755 # Octal integer /// 0b1010 # Binary integer /// 42u8 # Decimal number with type suffix /// 42f32 # Decimal number with type suffix e_num: struct { value: CIR.IntValue, kind: CIR.NumKind, },e_num携带value(CIR.IntValue,即 16 字节的紧凑整数载荷)与kind(CIR.NumKind)。在 src/canonicalize/Expression.zig 的pushToSExprTree中,e_num被序列化为e-num并附带value键值对,这正对应快照里的(e-num (value "255"))。
规范化背后的进制解析实现
0xFF → 255的换算发生在数字字面量解析阶段,实现在 src/parse/NumericLiteral.zig。该文件头部注释说明:数字语法在规范化之前就由解析器解释完毕,后续阶段只消费解析结果,不再重新解析数字 token 文本。
numberTextEnd(src/parse/NumericLiteral.zig)负责识别进制前缀并划定数字正文的边界:
- 以
0开头且紧随其后是x/X时,进制(radix)为 16; - 同理,
o/O对应 8 进制,b/B对应 2 进制; - 边界内允许出现
_(下划线数字分隔符),但一旦遇到不属于该进制的字符就停止。
digitValue(src/parse/NumericLiteral.zig)负责把字符映射为数值:'0'...'9'映射为 0–9,'a'...'f'与'A'...'F'映射为 10–15。这意味着Roc 的十六进制字面量同时支持大写和小写的a-f数字,也支持0x与0X两种前缀写法(0xFF和0XFF等价)。
compactInt(src/parse/NumericLiteral.zig)负责把数字正文换算为紧凑的 128 位载荷:
- 先识别可选的负号,再按进制逐位累乘累加(
parseUnsignedMagnitude,使用@mulWithOverflow/@addWithOverflow检测溢出); - 结果能装进
i128时记为i128载荷,否则若装得进u128则记为u128载荷; - 两种都装不下时,字面量走"精确路径"(
Compact.exact),其精确值以 base-256 大端字节串形式存进ModuleEnv的数字表,供后续from_numeral类型约束使用(对应 CIR 的e_num_from_numeral节点,见 src/canonicalize/Expression.zig)。
对于0xFF,换算结果 255 远小于i128上限,因此直接以紧凑i128载荷落入e_num,value即为"255"。此外,sourceDigitsMayFitBase256(src/parse/NumericLiteral.zig)为各进制设定了可物化的最大源数字位数,其中 16 进制的上限是max_numeral_digit_bytes × 2位,超出部分走非物化的精确表示。
单元测试对十六进制规范化的印证
src/canonicalize/test/int_test.zig 中专门有一个 "hexadecimal integer literals" 测试,覆盖了远超快照的十六进制用例:
- 基本用例:
0x0、0x1、0xFF→ 255、0x100、0xFFFF、0xFFFFFFFF; - 带下划线:
0x1_000→ 4096、0xFF_FF→ 65535、0x1234_5678_9ABC_DEF0; - 负十六进制:
-0x1、-0x80→ -128、-0x8000000000000000→ -9223372036854775808 等。
每个用例都会把字面量解析、规范化后断言e_num载荷的i128值。这说明快照中的0xFF只是十六进制支持的一个缩影:Roc 对十六进制(乃至二进制0b、八进制0o)的负数、下划线分隔、超长值均有完整的解析与规范化支持。
类型推断(TYPES):未标注字面量默认解析为 Dec
快照的TYPES段给出类型推断结果:
(inferred-types (defs (patt (type "Dec"))) (expressions (expr (type "Dec"))))声明模式x与右侧表达式0xFF都被推断为Dec类型。这与 src/check/test/num_type_inference_test.zig 中 "infers type for hex literals" 测试的断言完全一致——该测试对0x0、0x1、0xFF、0x100、0xFFFF等一系列十六进制字面量逐一验证"Number literals resolve to Dec after finalization"(数字字面量在终结化后解析为Dec)。
为什么默认是 Dec?
这背后是 Roc 数字字面量的类型系统设计。根据 src/check/test/num_type_inference_test.zig 的模块注释:当前类型系统中的数字字面量被表示为灵活类型变量,并带有针对from_numeral方法的静态分发约束。
在 src/types/numeral.zig 中,字面量可以转换到的内建数值类型集合Target定义为:
pub const Target = enum(u4) { u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, f32, f64, dec, };对每个字面量,类型系统会计算它能精确表示的候选类型集合FitSet(computeFitSet为每个字面量计算一次,两个字面量类型变量合一时取交集,见 src/types/numeral.zig)。当没有任何外部约束(如显式类型标注、函数参数期望类型)时,字面量变量在终结化阶段默认落向Dec——这正是x = 0xFF这种无上下文场景得到Dec的原因。同时intTargetAccepts(src/types/numeral.zig)按各整数类型的最大值/最小值(对有符号类型还包括负向边界)判断字面量能否被某个目标类型精确表示,f32/f64/dec对纯整数路径返回 false,交由各自的精确表示逻辑处理。
关于Dec的精度约束,src/types/numeral.zig 明确注释:Dec的小数精度为 18 位(值按 10^18 缩放),该常量与builtins.dec.RocDec.decimal_places保持一致。对于0xFF = 255这样的小整数,能精确表示且无需任何舍入,因此零告警地完成推断。
如何运行与维护这类快照测试
如果你希望亲自验证本文的结论,或在修改编译器后更新快照,可以按 test/snapshots/README.md 提供的方式操作:
# 生成(更新)全部快照 zig build run-snapshot-tool # 只处理某一个快照文件 zig build run-snapshot-tool -- test/snapshots/can_hex_integer.md # 用当前编译器输出覆盖 EXPECTED 段(用于确认预期已变化) zig build run-snapshot-tool -- test/snapshots/can_hex_integer.md --update-expected # 调试 REPL 类型快照时追踪解释器执行 zig build run-snapshot-tool -- <repl_snapshot.md> --trace-eval快照工具的实现在 src/snapshot_tool/main.zig:它编译SOURCE后按阶段生成TOKENS、PARSE、FORMATTED、CANONICALIZE、TYPES等段落,并把诊断报告序列化进PROBLEMS(语义以规范 S 表达式为准,与渲染器无关;渲染输出由type=reporting的独立快照另行固定)。NIL意味着没有报告。因此,can_hex_integer.md不只是一份测试数据,更是一份可复现的、带源码依据的十六进制字面量行为规范。
小结
通过逐段解读can_hex_integer.md并对照源码,可以得出以下可验证的结论:
0xFF在词法阶段是单个Inttoken,在语法阶段形成e-int表达式节点,全程零诊断;- 规范化阶段由 src/parse/NumericLiteral.zig 负责把
0xFF换算为十进制 255,存入 CIR 的e_num节点;同模块还支持0b/0o、下划线分隔、负号以及超出 128 位的精确路径; - 未受约束的数字字面量在类型推断终结化后默认解析为
Dec,其依据在 src/check/test/num_type_inference_test.zig 与 src/types/numeral.zig 的类型候选集(FitSet)机制; - 快照测试的生成与更新由
zig build run-snapshot-tool驱动,工具代码位于 src/snapshot_tool/main.zig。
如果想让0xFF落在具体类型上,只需提供约束即可,例如把它传给期望U64参数的函数,或用显式标注限定目标类型——届时类型系统会依据intTargetAccepts的边界判断该值能否精确表示。这份快照虽短,却是理解 Roc 数字字面量"从文本到类型"全链路的最佳入口。
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考