多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Roc 整数字面量下划线分隔符:语法规范与编译器快照测试全解析

Roc 整数字面量下划线分隔符:语法规范与编译器快照测试全解析 Roc 整数字面量下划线分隔符语法规范与编译器快照测试全解析【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/rocRoc 语言允许在数字字面量中使用下划线underscore作为千位分隔符例如1_000_000用于提升长数字的可读性而编译器会在解析阶段直接忽略这些分隔符。本文以 Roc 编译器仓库中的快照测试文件 test/snapshots/expr/int_with_underscores.md 为骨架结合 数字语言参考 的语法规范与 数字字面量解析实现 的源码证据完整讲解下划线数字的语法约束、词法/语法/规范化/类型推断各阶段的处理结果以及如何用快照测试驱动编译器行为验证。读完本文你将掌握 Roc 数字分隔符的完整规则与可验证的测试手段。一、快照文件是什么Roc 编译管线的逐阶段观测test/snapshots/expr/int_with_underscores.md属于 Roc 编译器仓库的快照测试snapshot test体系。根据 test/snapshots/README.md这类测试通过捕获一段 Roc 源码在每个编译阶段的输出来验证编译器行为Snapshot tests provide comprehensive validation of the compilation pipeline by showing how source code is transformed through each stage: tokenization, parsing, canonicalization, and type checking etc.也就是说一个快照文件会同时固定住同一段源码在词法分析TOKENS→ 语法分析PARSE→ 格式化FORMATTED→ 规范化CANONICALIZE→ 类型检查TYPES各阶段的中间产物。当编译器行为意外变化时这些文件能第一时间暴露回归。本快照的 META 区块声明了它的身份descriptionInteger literal with underscores typeexprtypeexpr表示这是一个“表达式级”普通快照其PROBLEMS区块记录的是诊断报告的语义S-expression 序列化不含渲染细节。二、语法规则下划线放哪里才合法根据 docs/langref/numbers.md 的数字字面量规范Roc 的数字字面量可以包含任意组合的数字字符、字母数字a-f用于十六进制、进制前缀、科学计数法后缀、小数点、下划线与前导负号。关于下划线官方语言参考的表述非常明确下划线仅用于让长数字更易读编译器会直接跳过它们the compiler skips over these它们可以出现在任意数字之间包括字母数字十六进制 a-f以及小数点后的数字之间每一下划线两侧必须各有一个数字each underscore must always have a digit on either side of it。因此下面这些写法都是合法的1_000_000 # 一百万的经典写法 -1_000_000.123_456_789 # 负小数同样可以用 0x12_34_56_78 # 十六进制中分隔字母数字之间也允许 1_000.000_1 # 小数点两侧均可分隔而诸如1_、_1、1__2这类两侧没有数字的下划线写法则不符合规范。这一约束在源码中有直接对应src/parse/NumericLiteral.zig中的isDecDigitOrUnderscore只把十进制数字与下划线视为数字字面量的有效字符而实际数值计算会跳过下划线详见下文第四节。三、逐阶段解剖1_000_000的完整旅程快照文件的核心价值在于它把1_000_000这一个字面量在编译管线中的每一步都白纸黑字地固定了下来。我们逐段解读。3.1 SOURCE 与诊断结果1_000_000# EXPECTED NIL # PROBLEMS NILEXPECTED为NIL表示该快照预期编译成功PROBLEMS为NIL表示编译器没有产生任何诊断报告既无错误也无警告。这正是下划线分隔符的正常形态它纯粹是语法糖不触发任何编译问题。3.2 词法分析TOKENSInt, EndOfFile,词法阶段只产出一个Int记号token外加文件结束记号EndOfFile。值得注意的是1_000_000被整体识别为一个Inttoken而不是被拆分成1、000、000三个数字——下划线被当作数字字面量的内部字符处理词法器不会把它当作运算符或标识符字符。对比 test/snapshots/expr/float_scientific.md 中1.23e-4的词法结果是Floattoken可见词法器会根据数字形态是否含小数点/指数区分Int与Float而下划线不影响这种分类。3.3 语法分析PARSE(e-int (raw 1_000_000))语法阶段产生一个整数表达式节点e-int其中raw字段保留的是源码原始文本1_000_000——也就是说语法树在这一步并不剥离下划线而是保留原始拼写把“去掉下划线”的工作留给了后面的阶段。这印证了词法器与解析器只做结构切分不做数值解释的设计数值语义的解释被明确下沉到src/parse/NumericLiteral.zig该文件头部注释写道Numeric syntax is interpreted here, before canonicalization. Later stages consume these facts; they must not parse number token text again.即数值语法在规范化之前就被解释后续阶段只消费解析结果不再重新解析数字 token 文本。3.4 格式化FORMATTEDNO CHANGE格式化阶段Roc 的代码格式化器对1_000_000不做任何改写。这说明下划线分隔符是风格上被认可且稳定保留的写法——格式化器不会把1_000_000改写成1000000也不会重排分隔位置。开发者可以放心用下划线组织数字不必担心格式化器破坏排版。3.5 规范化CANONICALIZE(e-num (value 1000000))规范化canonicalize阶段是下划线真正“消失”的地方e-int节点被转换为数值表达式e-num其value已经变成去掉下划线后的十进制字符串1000000。也就是说从规范化开始编译器的后续阶段类型检查、代码生成看到的都是纯净的数字表示下划线不会再传递下去。这一行为与源码实现完全吻合。在 src/canonicalize/Can.zig 的runExprKernel中e-int表达式通过literal.compact分支被转换为 CIR 表达式.int |value| CIR.Expr{ .e_num .{ .value cirIntValue(value), .kind .num_unbound, } },其中cirIntValue把解析阶段算好的 128 位整数值投影到 CIR 侧recordNumeralLiteralForNode则把解析阶段保存的精确数字位base-256 编码登记到节点上供类型检查阶段消费。换句话说1_000_000早在解析阶段就已经被解析为数值1000000详见第四节规范化阶段只是把它包装成统一的e-num表示。3.6 类型推断TYPES(expr (type Dec))类型检查阶段给这个字面量推断出的类型是Dec十进制小数类型。这与 docs/langref/numbers.md 中“默认到Dec”的规则一致当一个数字字面量从未被任何上下文约束为具体类型时例如1_000_000独立成表达式、没有参与任何运算Roc 会默认使用内置的Dec类型。这一点可以和 test/snapshots/expr/int_hex.md 对照0xFF的规范化结果是(e-num (value 255))类型同样是Dec——十六进制、带下划线的十进制在“未被约束时默认Dec”这一点上行为完全一致。四、源码级原理下划线是如何被“跳过”的下划线数字的核心实现位于 src/parse/NumericLiteral.zig解析入口是pub fn parse(allocator, raw_text, kind)L202-L234。它同时产出两条信息流紧凑负载compact payload能装进 128 位i128/u128的整数走Compact.int快速路径对应规范化阶段的e-num精确数字位exact base-256 digits超出 128 位的超大数字走exact路径以 base-256 字节序列保存每一位支持任意大数。下划线在这两条路径中都被一致地忽略1. 快速路径的跳过逻辑——parseUnsignedMagnitudeL723-L740fn parseUnsignedMagnitude(digits: []const u8, radix: u8) ?u128 { var value: u128 0; var saw_digit false; for (digits) |byte| { if (byte _) continue; // 下划线直接跳过不参与数值计算 const digit digitValue(byte) orelse return null; ...compactIntL680-L721随后把这段无下划线的数字按进制累乘得到 i128/u128 值。对于1_000_000结果就是1000000这正是快照中e-num (value 1000000)的来源。2. 精确路径的计数规则——sourceDigitsMayFitBase256L664-L678在估算数字长度时同样跳过下划线for (digits) |byte| { if (byte ! _) digit_count 1; }这意味着下划线不占用数字位数的预算max_numeral_digit_bytes定义了精确数字列表的最大字节数从而不会影响超大数字字面量的可表达范围。3. 词法扫描器的字符集—— 数字 token 的扫描由isDecDigitOrUnderscoreL337-L339驱动fn isDecDigitOrUnderscore(byte: u8) bool { return (byte 0 and byte 9) or byte _; }词法器在扫描数字时会连续吞下数字与下划线直到遇到其他字符才收尾因此1_000_000整体成为一个Inttoken。4. 单位测试印证——NumericLiteral.zig自带的测试parse exact integers keeps underscore-separated base digitsL1099-L1111直接验证了各进制的下划线行为var hex try parse(std.testing.allocator, 0x12_34_56_78, .int); try std.testing.expectEqualSlices(u8, .{ 0x12, 0x34, 0x56, 0x78 }, hex.before); var binary try parse(std.testing.allocator, 0b1010_0101, .int); try std.testing.expectEqualSlices(u8, .{0xa5}, binary.before); var octal try parse(std.testing.allocator, 0o1_777, .int); try std.testing.expectEqualSlices(u8, .{ 0x03, 0xff }, octal.before);可见下划线分隔在十六进制0x12_34_56_78→0x12345678、二进制0b1010_0101→0b101001010xa5、八进制0o1_777→0o17770x3ff中同样生效——与语言参考中“字母数字之间也允许下划线”的规则一致。五、横向对照下划线在数字字面量家族中的位置将本快照与test/snapshots/expr/目录下的其他数字字面量快照并排观察可以清晰看出下划线与其他数字语法特性的关系快照文件源码词法 token规范化结果类型int_with_underscores.md1_000_000Int(e-num (value 1000000))Decint_hex.md0xFFInt(e-num (value 255))Decfloat_scientific.md1.23e-4Float(e-dec-small (numerator 123) (denominator-power-of-ten 6) (value 0.000123))Decfloat_negative.md-2.5Float(e-dec-small (numerator -25) (denominator-power-of-ten 1) (value -2.5))Decmin_parens_number.md-(8)OpUnaryMinus,NoSpaceOpenRound,Int,CloseRound(e-dispatch-call (method negate) ... (receiver (e-num (value 8))))Dec几点关键差异值得注意下划线不改变 token 类别1_000_000与0xFF一样是Int而含小数点/指数的写法才是Float。下划线是纯可读性语法它不影响数值本身1_000_000≡1000000而进制前缀0x与科学计数法e-4会实质改变数值语义。负号的两种形态快照 min_parens_number.md 展示了-(8)被解析为一元负号运算(unary - ...)并最终规范化为e-dispatch-call (method negate)而-1_000_000这样的写法是字面量自带的负号词法层面进入数字 token不会产生 negate 调用。语言参考特别强调了这个区别对自定义数字类型的影响。另外int_large.md 给出了一个有趣的边界案例99999999999999999999999999999930 个 9在未被约束时默认类型为Dec但该值超出Dec的表示范围于是产生runtime_error级别的 Invalid Number 诊断报告。这说明数字字面量是否合法取决于推断出的目标类型下划线分隔符本身不改变这一点——把超大数字写成999_999_999_999_999_999_999_999_999_999并不会让它变得合法。六、运行与维护快照验证编译行为的标准姿势按照 test/snapshots/README.md快照测试由 Zig 构建系统驱动常用命令如下# 生成/更新全部快照 zig build run-snapshot-tool # 仅更新指定快照文件 zig build run-snapshot-tool -- test/snapshots/expr/int_with_underscores.md # 基于诊断结果更新期望值 zig build run-snapshot-tool -- test/snapshots/expr/int_with_underscores.md --update-expected当编译器实现发生改动时快照文件会暴露每个阶段的差异例如若解析器不再跳过下划线CANONICALIZE区块的(e-num (value 1000000))就会变成其他值测试随即失败从而精准定位回归点。关于快照结构还有两点补充语义与渲染分离普通快照typeexpr等的PROBLEMS区块存放诊断的语义 S-expression见 src/reporting/report_sexpr.zig 的序列化而reporting/目录下的渲染快照才固定 CLI/Markdown/HTML/LSP 等输出格式。下划线数字无诊断PROBLEMS为NIL因此本文件不涉及渲染细节。快照后处理快照后处理会全局重写被移除的 header 关键字这一规则同样作用于 S-expression 输出内部。七、实战要点总结基于上述文档、源码与快照证据使用 Roc 下划线数字字面量时请记住以下要点纯装饰、零成本下划线不影响数值、token 类别与类型推断从规范化阶段起即被剥离src/canonicalize/Can.zig。位置约束严格下划线两侧必须有数字可用于十进制、十六进制/八进制/二进制数字之间以及小数点后docs/langref/numbers.md。格式化器会保留FORMATTED结果为NO CHANGE不用担心被改写。负号写法-1_000_000是字面量的一部分而-(1_000_000)是 negate 运算对比 min_parens_number.md。不改变合法边界数字是否“装得下”取决于推断类型如未约束时默认Dec见 int_large.md 的溢出诊断下划线不参与数值判定。可验证任何数字字面量的编译行为都可以通过zig build run-snapshot-tool以快照方式固化和回归验证test/snapshots/README.md。推荐继续阅读仓库中的相关材料数字语言参考完整数字类型表与自定义数字类型、快照测试说明快照机制全貌、数字字面量解析实现下划线及进制处理源码、规范化入口e-num的生成逻辑以及test/snapshots/expr/目录下的 int_hex.md、float_scientific.md、int_large.md 等兄弟快照。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表