frontend/gda_expr 设计

设计目标

通用十进制算术(General Decimal Arithmetic)规范11 M. F. Cowlishaw, General Decimal Arithmetic Specification, version 1.70,以及随附的 decTest 套件。IEEE 754-2019 的十进制格式采用了相同的算术。 附带一个由 .decTest 文件组成的大型语料:每一行给出一个操作、其操作数、上下文、精确的预期结果,以及它必须引发的确切条件集合。本包将该语料转化为关于 decimal_gda 的可执行的有限结论:只有当 decimal_gda 产生相同的表示和相同的条件时,一行才算通过。本包是纯的(输入文本,输出汇总),因此可以在进程内测试、分片,并由一个精简的 CLI 驱动。

数学背景

十进制数据与上下文

一个有限 GDA 数是一个三元组 (s,c,q)(s, c, q),其中符号 s∈{0,1}s \in \{0, 1\},整数系数 c≥0c \ge 0,指数为 qq,其值为 (−1)s⋅c⋅10q(-1)^s \cdot c \cdot 10^{q}。不同的三元组可能具有相同的值:同一个值的所有表示构成其同值类(cohort),例如 2.02.0 和 2.002.00 分别对应 (0,20,−1)(0, 20, -1) 和 (0,200,−2)(0, 200, -2)。规范规定了每个操作返回同值类中的哪个成员(理想指数),因此测试行的预期结果是一个表示,而不仅仅是一个值。特殊值包括 ±∞\pm\infty,以及带整数载荷和符号的静默或信号 NaN。

上下文 κ=(p,ρ,Emin⁡,Emax⁡,clamp,extended)\kappa = (p, \rho, E_{\min}, E_{\max}, \mathit{clamp}, \mathit{extended}) 给出精度、舍入模式和指数范围。操作 ff 计算精确结果,在 κ\kappa 下对其舍入,并引发十三种条件的一个子集:Inexact、Rounded、Lost_digits、Invalid_operation、Division_by_zero、Overflow、Underflow、Subnormal、Clamped、Conversion_syntax、Division_impossible、Division_undefined 和 Invalid_context。

行即操作

一行

id  op  a1 … an  ->  x  c1 … cm

在指令上下文 κ\kappa 下被降低为 numeric_expr 的表达式 op(a1,…,an)\mathsf{op}(a_1, \dots, a_n),并借助两个回调求值。字面量回调将每个 aja_j 解码为一个 @decimal_gda.Decimal:

  • # 后跟十六进制数字:IEEE 754 交换格式编码,按 (p,Emin⁡,Emax⁡)(p, E_{\min}, E_{\max}) 与上下文相等的格式解码((7,−95,96)(7, -95, 96)、(16,−383,384)(16, -383, 384) 或 (34,−6143,6144)(34, -6143, 6144));在其他任何上下文中该操作数无效;
  • 32#…、64#…、128#…:舍入到相应交换格式的十进制文本;
  • 否则为十进制文本(去掉开头的 +),以精度 max⁡(64,p)\max(64, p) 解析,这使得不超过该位数的每个操作数都保持精确。

操作回调将规范化后的名称映射到一个 decimal_gda 函数(例如 add 映射到 @decimal_gda.add,squareroot 映射到 @decimal_gda.sqrt,comparetotal 映射到 @decimal_gda.compare_total),并以由 κ\kappa 构造、禁用所有陷阱的 @decimal_gda.GdaContext 调用它。结果是四种类型之一的值(十进制数、整数、布尔值、文本),以及所引发条件的集合 FF。转换 tosci 和 toeng 读取原始操作数文本,因为从文本的转换本身就是被测对象。仅针对交换格式的操作(canonical,以及作用于 # 操作数并产生 # 结果的 apply、copy*)直接处理编码并返回文本。

通过规则

设 vv 为实际结果,FF 为实际条件集合,CC 为所列条件的集合。定义 C^\widehat{C}:当 CC 包含 Division_impossible 或 Division_undefined 时向其加入 Invalid_operation(两者都通过无效操作信号报告)。该行通过当且仅当

match⁡(v,x)  ∧  F=C^,\operatorname{match}(v, x) \;\wedge\; F = \widehat{C},

其中标志集合的相等性在两个方向上检查:缺少或多出一个条件都会使该行失败。match⁡\operatorname{match} 取决于预期记号 xx 和 vv 的类型:

预期 xx实际 vvmatch⁡(v,x)\operatorname{match}(v, x)
?任意真(只检查条件)
#hex十进制数vv 在上下文格式中的交换格式编码等于 xx,忽略字母大小写
32#…, 64#…, 128#…十进制数vv 在数值上等于 xx 在该格式中的值(compare == 0);将 vv 编码为该格式时引发的条件会加入 FF
其他文本十进制数vv 的打印结果与 xx 完全相同,或 compareTotal⁡(v,dec⁡(x))=0\operatorname{compareTotal}(v, \operatorname{dec}(x)) = 0
其他文本整数vv 的十进制文本等于 xx
其他文本布尔xx 为 true/1 表示真,为 false/0 表示假,忽略大小写
文本文本字符串相等(对 #hex 忽略大小写)

十进制情形在表示层面是精确的。IEEE 754 totalOrder22 IEEE 754-2019,第 5.10 节,totalOrder。Decimal::compare_total 为 decimal_gda 实现了它。 先比较符号,再比较类别,然后比较数值,最后比较指数,并按信号位和载荷对 NaN 排序。因此

compareTotal⁡(a,b)=0  ⟺  {(sa,ca,qa)=(sb,cb,qb)finite,sa=sb±∞,sa=sb, same signaling bit and payloadNaN,\operatorname{compareTotal}(a, b) = 0 \iff \begin{cases} (s_a, c_a, q_a) = (s_b, c_b, q_b) & \text{finite,} \\ s_a = s_b & \pm\infty, \\ s_a = s_b,\ \text{same signaling bit and payload} & \text{NaN,} \end{cases}

所以 2.0 与 2.00 不匹配,-0 与 0 不匹配,NaN12 与 NaN 也不匹配。

设计决策

对不是测试的行给出处置类别而非判为失败

问题。 官方语料包含一些并不描述标量计算的行:用于无效或非标量编码的 # 占位符、来自旧版本的 ? 操作数、库未提供的操作,以及库不认识的舍入模式。将它们计为失败会掩盖真正的失败;静默丢弃它们则会夸大覆盖率。

选择。 每个被选中的行都会得到一个处置类别。Diagnostic 标记按构造即不可执行的行(# 或 ? 操作数、# 结果)。Unsupported 标记库无法运行的合法行(未知的操作、条件或舍入)。只有 Executable 行可以通过或失败,汇总报告每一类,因此诸如“所有可执行行都通过”这样的结论会附带它所排除的行数。严格性(有任何不支持项即失败)是调用者的策略:RunOptions 记录它,CLI 将其应用于退出码。

禁用陷阱,比较条件

.decTest 行列出的是条件,而不是陷阱。以空陷阱集运行每个操作,可使 decimal_gda 返回规定的默认结果(例如无效操作时返回静默 NaN)并报告所有引发的条件,这正是该行所陈述的内容。在两个方向上比较整个集合,可以同时捕获缺失的和多余的 Inexact、Rounded、Clamped 等条件。

表示精确的比较

GDA 规定了每个结果的指数,因此一个返回正确值但指数错误的库就是错误的。用 compare_total 或精确字符串进行比较,会将同值类、零的符号和 NaN 载荷都纳入测试。唯一的值层面比较用于 32#/64#/128# 结果,此时该行测试的是编码条件。

指令上下文仅在变化时解析一次

每一行都保存其所在行生效的指令记录。execute_documents 只在记录与前一行不同时才将其转换为 decimal_gda 上下文。转换是记录的纯函数,因此缓存永远不会改变结果;它消除了在同一上下文下包含数千行的文件中的重复工作。

确定性分片

问题。 语料很大,并在多个并行进程中运行,这些进程的结果相加必须等于串行运行的结果。

选择。 过滤之后,行按文档顺序编号为 k=0,1,…,N−1k = 0, 1, \dots, N-1,nn 个分片中的第 ii 个分片取 Si={k:k mod n=i}S_i = \{k : k \bmod n = i\}。轮转分配会把含有慢操作(power、ln)的文件分散到各个分片,而不是让某一个分片承担整个慢文件。

正确性 / 不变式

划分。 对于 n≥1n \ge 1,集合 S0,…,Sn−1S_0, \dots, S_{n-1} 两两不交且覆盖 {0,…,N−1}\{0, \dots, N-1\},因为每个 kk 模 nn 恰有一个余数。它们的大小是均衡的:

∣Si∣=⌈N−in⌉∈{⌊Nn⌋,⌈Nn⌉}.|S_i| = \left\lceil \frac{N - i}{n} \right\rceil \in \left\{ \left\lfloor \frac{N}{n} \right\rfloor, \left\lceil \frac{N}{n} \right\rceil \right\}.

分片独立性。 一行的处置类别和结果只取决于该行本身(其记号和指令记录):解析为每一行确定记录,而上下文缓存记忆的是一个纯函数。因此,第 kk 行的结果在包含它的每个分片中以及在串行运行中都相同,并且

merge⁡(R0,…,Rn−1) has the counters of Rserial,\operatorname{merge}(R_0, \dots, R_{n-1}) \text{ has the counters of } R_{\text{serial}},

因为 merge 将每个计数器相加,而各分片构成行的一个划分(total_cases 在每个分片中都是同一个 NN,merge 会保留它)。

计数器恒等式。 对每个汇总,selected=executable+skipped\text{selected} = \text{executable} + \text{skipped},executable=passed+failed\text{executable} = \text{passed} + \text{failed},skipped=diagnostic+legacy+unsupported\text{skipped} = \text{diagnostic} + \text{legacy} + \text{unsupported};这些等式成立,是因为每个结果都被 internal/conformance 恰好计入一个类别。

完全性。 解析将每一行恰好归入以下之一:跳过(空行或注释)、指令、测试行、诊断。执行永远不会因行内容而中止:解码和分派失败会成为带有消息 "evaluation failed" 的失败结果。

复杂度。 解析与文本长度呈线性关系。执行与行数呈线性关系,再加上十进制运算本身的开销。

被否决的替代方案

  • 仅比较值。 更简单,但会接受错误的指数和零的符号,而规范和语料恰恰有意测试这些。
  • 条件的子集比较(只要求引发所列的条件)。这会接受多余的 Inexact 或 Clamped,而这是一类常见的缺陷。
  • 以临时约定的含义执行 # 和 ? 行。 这些行没有标量含义;凭空赋予一个含义会使通过计数失去意义。
  • 连续分片。 将行列表切分为块同样是确定性的,但会把整个慢文件放进同一个分片。

边界

  • 不负责文件系统访问、通配符展开或进程退出码:这些属于 cli/gda_expr_cli 和 tools/。
  • 不使用陷阱:执行各行时禁用所有陷阱。
  • 有效数字超过 max⁡(64,p)\max(64, p) 位的操作数在解码时按就近舍入(偶数优先)处理。
  • 不识别带引号的 decTest 字符串中的 '' 转义。
  • Legacy 是共享结果模型的一部分,但当前的执行器从不分配它。
  • 该执行器只测试 decimal_gda;IEEE 十进制(decimal)在 tools/ 中有自己的语料运行器。

Footnotes

  1. M. F. Cowlishaw, General Decimal Arithmetic Specification, version 1.70,以及随附的 decTest 套件。IEEE 754-2019 的十进制格式采用了相同的算术。 ↩

  2. IEEE 754-2019,第 5.10 节,totalOrder。Decimal::compare_total 为 decimal_gda 实现了它。 ↩