Roc 编译器中的 dec_small 小十进制字面量表示:以 327.67 快照为例解析精确有理数编码边界
2026/9/19 20:39:06 网站建设 项目流程

【免费下载链接】roc

A fast, friendly, functional language.

项目地址:https://gitcode.com/GitHub_Trending/ro/roc
点击查看免费下载

Roc(A fast, friendly, functional language)为小数运算引入了独立的Dec类型,并在编译器前端实现了内存紧凑的dec_small快速路径:当一个十进制字面量的分子恰好落在 i16 范围内时,编译器不再用 i128 缩放表示,而是把它编码为「分子 / 10^n」的有理数对。本文以仓库快照测试 test/snapshots/numeric_edge_cases/dec_small_max_value.md 中327.67这一字面量为例,逐阶段拆解它从词法分析到类型检查的完整编译流水线输出,并结合 src/parse/NumericLiteral.zig、src/canonicalize/Expression.zig 等源码,说明 dec_small 的边界规则(i16 最大值 32767、最小值 -32768)、退避(fallback)机制,以及它如何帮助 Roc 避免0.1 + 0.2 = 0.30000000000000004这类浮点精度问题。读完本文,你将能读懂 Roc 编译器仓库中任意 numeric_edge_cases 快照文件,并理解小数字面量在 Roc 内部的精确表示与类型推导路径。

快照文件的整体结构:一次编译行为的完整记录

test/snapshots/numeric_edge_cases/目录下存放着一组针对数值边界条件的编译器快照测试。每个快照文件都是一份"编译过程契约":它把一段 Roc 源码在编译流水线各阶段产生的确定性输出固定下来,任何阶段的行为变化都会导致快照失配,从而在 CI 中暴露回归。快照的通用说明见 test/snapshots/README.md:快照捕获的是 tokenization、parsing、canonicalization、type checking 等各阶段对源码的变换结果。

dec_small_max_value.md为例,它由十个有序小节组成,从上到下正好对应编译流水线的推进顺序:

小节内容对应阶段
METAini 格式的元信息:descriptiontype快照分类
SOURCE被测试的 Roc 源码片段输入
EXPECTED/PROBLEMS期望的诊断结果(NIL表示无诊断)诊断输出
TOKENS词法分析产出的 Token 序列tokenize
PARSE语法分析产出的 AST(S-expression)parse
FORMATTED格式化器输出(NO CHANGE表示已是规范格式)format
CANONICALIZE规范化后 CIR 的 S-expressioncanonicalize
TYPES类型检查推导出的最终类型type check
# META description=Maximum positive value that fits in dec_small (i16 max) type=expr

type=expr表示这是一个表达式快照:SOURCE中是一个独立表达式而非完整文件。PROBLEMSNIL说明该表达式在编译全程不产生任何诊断(无类型错误、无无效字面量告警)。

SOURCE 与词法阶段:327.67 被识别为单个 Float 字面量

快照的SOURCE极其简短:

327.67

词法分析把它整体识别为一个FloatToken,后面紧跟EndOfFile

Float, EndOfFile,

注意这里只有一个 Token,而不是327.67三个 Token——小数点在 Roc 词法层就被并入字面量。这是TOKENS小节与PARSE小节能够成立的前提:解析器拿到的是一个完整的数字 Token,而不是拼接出来的碎片。

PARSE 阶段:e-frac 表达式节点

语法分析把该字面量产出为e-frac(fractional literal)AST 节点,并保留原始文本:

(e-frac (raw "327.67"))

e-frac是 Roc AST 中表示"小数/分数面量"的统一节点,无论字面量后续走 dec_small、Dec 还是精确 numeral 路径,解析阶段都不做区分,只忠实记录原始文本。这种"解析层宽松、规范化层分流"的设计,把数值语义的决策推迟到了 canonicalize 阶段。

CANONICALIZE 阶段:dec_small 与有理数编码

规范化是本文的核心。327.67在 canonicalize 阶段被投影为:

(e-dec-small (numerator "32767") (denominator-power-of-ten "2") (value "327.67"))

三个字段的含义一目了然:

  • numerator:分子 32767;
  • denominator-power-of-ten:分母是 10 的 2 次方(即 100);
  • value:恢复出的十进制显示值 327.67。

也就是说,327.67被精确存储为有理数32767 / 10²,而不是任何浮点近似。这就是e_dec_small节点:一个把"常见小数字面量"压进 3 字节(i16 分子 + u8 指数)的紧凑表示。

源码中的定义

该 CIR 节点在 src/canonicalize/Expression.zig 中定义:

/// A small decimal literal stored as a rational number (numerator/10^denominator). /// Memory-efficient representation for common decimal values. /// Avoids floating-point precision issues by using exact rational arithmetic. /// /// ```roc /// 3.14 # Stored as numerator=314, denominator_power_of_ten=2 (314/100) /// 0.5 # Stored as numerator=5, denominator_power_of_ten=1 (5/10) /// 42.0 # Stored as numerator=42, denominator_power_of_ten=0 /// ``` e_dec_small: struct { value: CIR.SmallDecValue, has_suffix: bool, // If the value had a `dec` suffix },

而载荷类型SmallDecValue在 src/parse/NumericLiteral.zig 定义为:

/// Compact decimal payload for small fractional literals. pub const SmallDecValue = struct { numerator: i16, denominator_power_of_ten: u8, };

i16分子决定了 dec_small 的数值边界:分子绝对值最大为 32767,最小负值为 -32768,这正是快照description中 "(i16 max)" 的由来。

边界判定逻辑

解析器在 src/parse/NumericLiteral.zig 的smallDecFromParts中执行边界检查:逐位累加分子并乘以 10,任何一步超过maxInt(i16)(对负数还要加上符号余量)都会返回null,从而放弃 dec_small 路径:

fn smallDecFromParts(before: []const u8, after: []const u8, after_count: u64, is_negative: bool) ?SmallDecValue { // A scale beyond Dec's 18 decimal places is not representable as a // small-dec fast path (it would silently shift the value); such literals // take the exact path and fail Dec fit validation instead. if (after_count > 18) return null; ... if (magnitude > @as(u32, @intCast(std.math.maxInt(i16))) + @intFromBool(is_negative)) return null; ... const signed: i32 = if (is_negative) -@as(i32, @intCast(magnitude)) else @intCast(magnitude); if (signed < std.math.minInt(i16) or signed > std.math.maxInt(i16)) return null; return .{ .numerator = @intCast(signed), .denominator_power_of_ten = @intCast(after_count), }; }

这段代码还揭示了一个隐蔽约束:小数位(after_count)超过 18 时直接返回null,因为 Dec 的固定精度是 18 位小数,更大的 scale 会静默移位数值——这类字面量必须走精确 numeral 路径(exact)并在 Dec 适配校验中报错,而不是被错误地压进紧凑表示。

compactFrac(src/parse/NumericLiteral.zig)按优先级依次尝试small_decdec(i128 缩放表示)→exact,形成三级退避链。规范化阶段 src/canonicalize/Can.zig 依据literal.compact的判别值把small_dec投影为e_dec_smallCIR 节点,并通过 cirSmallDec 完成 parser 侧结构与 CIR 侧结构的搬运。

快照输出如何产生

CANONICALIZE小节中的 S-expression 并非手写,而是由 CIR 的 S-expression 序列化器生成,实现在 src/canonicalize/Expression.zig:依次输出e-dec-small原子名、numeratordenominator-power-of-ten字符串对,再以numerator_f64 / 10^denominator计算并格式化出value字段。这正是快照工具(见 test/snapshots/README.md 的zig build run-snapshot-tool)用来对比期望值的来源。

TYPES 阶段:Dec 类型的推导

类型检查对e_dec_small节点的处理位于 src/check/Check.zig:无后缀时走checkNumeralLiteral通用路径,通过from_numeral约束把字面量统一到 Numeral 类型,再按上下文解析;有dec后缀时则直接与内置Dec类型统一。快照最终记录:

(expr (type "Dec"))

327.67在无任何上下文注解的情况下被推导为Dec类型——Roc 的小数字面量默认类型就是Dec,与3.14.Dec这类显式后缀写法等价。

边界对照:与相邻快照组成的边界矩阵

单个快照只能说明一个点,但numeric_edge_cases目录把它们组织成了完整的边界矩阵,强烈建议对照阅读:

  • dec_small_min_value.md:-327.68(e-dec-small (numerator "-32768") (denominator-power-of-ten "2") (value "-327.68")),这是 i16 最小值,同样落在 dec_small 内;
  • dec_above_small_limit.md:32768.0(e-frac-dec (value "32768")),分子 327680 超过 i16 上限,退避到e-dec(i128 缩放)表示
  • dec_small_positive_tiny.md:0.0001(e-dec-small (numerator "1") (denominator-power-of-ten "4") (value "0.0001")),展示小数位被完整保留;
  • dec_small_negative.md、dec_negative_zero.md、dec_exact_power_of_ten.md、dec_trailing_zeros.md:覆盖负数、负零、10 的整次幂与尾随零等边界;
  • dec_scientific_large.md、dec_scientific_negative_exp.md:科学计数法字面量在 dec_small 与精确路径之间的取舍;
  • int_i16_max.md、int_i128_max.md 等:整数边界的对称矩阵。
327.67 # (e-dec-small (numerator "32767") (denominator-power-of-ten "2") ...) 32768.0 # 分子超界 → (e-frac-dec ...),退避为 i128 缩放的 Dec -327.68 # (e-dec-small (numerator "-32768") ...) 仍属 dec_small 0.0001 # (e-dec-small (numerator "1") (denominator-power-of-ten "4") ...)

一句话总结边界:分子在 [-32768, 32767] 内的十进制字面量走e_dec_small,分子绝对值 ≥ 32768 或小数位超过 18 位则分别退避到e_dec或精确 numeral 路径。

单元测试的独立佐证

快照之外,仓库还用单元测试直接断言了 dec_small 的行为,见 src/canonicalize/test/frac_test.zig:

  • 基础用例(3.14)断言规范化结果是e_dec_small,且numerator == 314denominator_power_of_ten == 2(frac_test.zig);
  • 负数科学计数法(-1.5e-5)断言numerator == -15denominator_power_of_ten == 6,即-15 / 10⁶(frac_test.zig);
  • 负零(-0.0e0)断言紧凑载荷的numerator == 0,同时精确 numeral 记录保留符号位(frac_test.zig);
  • 超界回退测试 "small dec - exceeds i16 range falls back to Dec" 使用32768.0,断言其 activeTag 为e_dec,注释明确写道 "Falls back to Dec because 32768 > 32767 (max i16)"(frac_test.zig)——与快照dec_above_small_limit.md的结果完全一致。

此外,src/canonicalize/CIR.zig 为SmallDecValue提供了两个转换方法:toF64(用于大小比较)与toRocDec(转换为 10¹⁸ 缩放的 i128 表示,底层由 src/builtins/dec.zig 的RocDec.fromFraction(numerator, denominator_power)实现)。这说明 dec_small 既是解析/规范化的紧凑存储格式,也是后续求值、类型推导的统一入口。

为什么需要 dec_small:精确十进制的设计动机

src/canonicalize/Expression.zig 中e_dec节点的注释给出了设计动机:

3.14159265358979323846 # High precision decimal 0.1 + 0.2 # Equals exactly 0.3 (not 0.30000000000000004)

在二进制浮点(F64)下0.1 + 0.2会得到0.30000000000000004;而 Roc 的Dec以十进制语义存储(i128 按 10¹⁸ 缩放),小数运算保持十进制精确。dec_small则是这个精确体系下的内存优化:对于分子能塞进 i16 的"常见小额"(如 3.14、0.5、42.0、327.67),用 3 字节有理数对即可表示,绝大多数源码中的字面量都能命中这一快速路径,从而减少大载荷的分配与拷贝。

如何运行与验证这些快照

如果你在本仓库环境中工作,可以按 test/snapshots/README.md 的说明实际操作:

# 生成/更新全部快照 zig build run-snapshot-tool # 只更新指定快照 zig build run-snapshot-tool -- test/snapshots/numeric_edge_cases/dec_small_max_value.md # 期望值与 PROBLEMS 不一致时,以当前诊断结果刷新期望 zig build run-snapshot-tool -- test/snapshots/numeric_edge_cases/dec_small_max_value.md --update-expected

运行前需按 BUILDING_FROM_SOURCE.md 准备 Zig 工具链并完成zig build。改动dec_small相关源码后,若某个阶段的输出与快照不一致,工具会给出明确的差异提示,这正是快照测试在编译器开发中充当"行为契约"的意义所在。

小结

dec_small_max_value.md用 33 行快照完整记录了327.67在 Roc 编译器中的旅程:单个FloatToken →e-fracAST →(e-dec-small (numerator "32767") (denominator-power-of-ten "2") (value "327.67"))→ 类型Dec。它既是一个边界条件测试(验证 i16 上限 32767),也是理解 Roc 十进制精确表示体系的绝佳入口。配合 src/parse/NumericLiteral.zig、src/canonicalize/Expression.zig、src/canonicalize/test/frac_test.zig 与numeric_edge_cases目录下的姊妹快照,你可以完整复原 Roc 从小数字面量到精确有理数表示、再到类型推导的整条决策链:小则dec_small(i16 有理数),中则e-dec(i128 缩放),大则精确 numeral 表——三级表示,各司其职。

【免费下载链接】roc

A fast, friendly, functional language.

项目地址:https://gitcode.com/GitHub_Trending/ro/roc
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询