gda_expr 教程
本教程介绍如何在 MoonBit 代码中针对 decimal_gda 运行 General Decimal Arithmetic 测试行(.decTest 文件):解析文档、执行它、读取摘要,并查明某一行失败的原因。本教程还介绍如何选择行以及如何把一次运行拆分为多个分片。若要在命令行中运行整套语料,请改用 gda_expr_cli 运行器。
快速入门
将该包添加到 moon.pkg:
import {
"Luna-Flow/floating/frontend/gda_expr",
}
解析两行并执行:
///|
test "quick start" {
let source =
#|precision: 9
#|rounding: half_even
#|add001 add 1.20 2 -> 3.20
#|div001 divide 1 3 -> 0.333333333 Inexact Rounded
#|
let document = @gda_expr.parse_dectest("quick.decTest", source).unwrap()
let summary = @gda_expr.execute_documents([document])
inspect(summary.passed_cases(), content="2")
inspect(summary.success(), content="true")
}
每一行的形式为 id operation operand… -> expected condition…。行上方的指令(precision:、rounding:、…)设定其后所有行使用的上下文。
日常任务
查明某一行失败的原因
只有结果与条件(状态标志)的精确集合都匹配时,一行才算通过。每个 CaseResult 都为失败附带一条消息:
///|
test "inspect failures" {
let source =
#|precision: 9
#|ok1 multiply 3 4 -> 12
#|bad1 add 1 1 -> 3
#|bad2 divide 1 3 -> 0.333333333
#|
let document = @gda_expr.parse_dectest("failures.decTest", source).unwrap()
let summary = @gda_expr.execute_documents([document])
inspect(summary.failed_cases(), content="2")
let messages = summary
.results()
.filter(r => !r.passed())
.map(r => r.message())
inspect(messages[0], content="result mismatch: expected 3, actual 2")
inspect(
messages[1],
content="status flags mismatch: expected , actual Inexact Rounded",
)
}
bad2 的数字正确,但缺少 Inexact Rounded;多出一个标志与缺少一个标志一样,都算失败。
结果连同指数一起比较
预期结果是作为十进制表示来比较的,而不仅仅作为数值:2.0 和 2.00 的值相同但指数不同,只有指数正确的那个才能通过。带符号零和 NaN 的载荷也以同样方式比较。
///|
test "exponent matters" {
let source =
#|precision: 9
#|q1 add 1.00 1.0 -> 2.00
#|q2 add 1.00 1.0 -> 2.0
#|z1 multiply -1 0 -> -0
#|z2 multiply -1 0 -> 0
#|
let document = @gda_expr.parse_dectest("cohorts.decTest", source).unwrap()
let passed = @gda_expr.execute_documents([document])
.results()
.map(r => r.id() + "=" + r.passed().to_string())
inspect(passed.join(" "), content="q1=true q2=false z1=true z2=false")
}
跳过的行与处置类型
有些行不会被执行。每个结果都有一个 CaseDisposition:Executable 行会被运行;Diagnostic 行(操作数或结果为 # 占位符,或操作数为 ?)和 Unsupported 行(未知的运算、条件或舍入模式)计为跳过,永远不会导致运行失败。
///|
test "dispositions" {
let source =
#|precision: 9
#|r1 add 1 1 -> 2
#|r2 add # 1 -> #
#|r3 frobnicate 1 -> 1
#|rounding: banker
#|r4 add 1 1 -> 2
#|
let document = @gda_expr.parse_dectest("mixed.decTest", source).unwrap()
let summary = @gda_expr.execute_documents([document])
inspect(summary.executable_cases(), content="1")
inspect(summary.diagnostic_cases(), content="1")
inspect(summary.unsupported_cases(), content="2")
inspect(summary.skipped_cases(), content="3")
inspect(summary.success(), content="true")
let reasons = summary
.results()
.filter_map(r => {
match r.disposition() {
@gda_expr.Unsupported(reason) => Some(r.id() + ": " + reason)
_ => None
}
})
inspect(
reasons.join("; "),
content="r3: unsupported operation frobnicate; r4: unsupported rounding banker",
)
}
success() 只表示没有可执行行失败。严格的运行器还要求没有任何不支持的内容;CLI 通过 --strict-supported 实现这一点。
选择行并将工作拆分为分片
RunOptions 按 id 过滤行,并从剩余的行中选出一个分片。过滤器是以逗号分隔的 id 或范围 first..last 列表,其中范围匹配长度相同、且排序位于上下界之间的 id:
///|
test "filter and shard" {
let source =
#|precision: 9
#|add001 add 1 1 -> 2
#|add002 add 2 2 -> 4
#|add003 add 3 3 -> 6
#|add004 add 4 4 -> 8
#|mul001 multiply 2 3 -> 6
#|
let document = @gda_expr.parse_dectest("select.decTest", source).unwrap()
let filtered = @gda_expr.execute_documents(
[document],
options=@gda_expr.RunOptions::new(case_filter="add002..add004,mul001"),
)
inspect(filtered.total_cases(), content="4")
let shards = [0, 1, 2].map(index => {
@gda_expr.execute_documents(
[document],
options=@gda_expr.RunOptions::new(shard_count=3, shard_index=index),
)
})
inspect(shards.map(s => s.selected_cases().to_string()).join(" "), content="2 2 1")
let merged = @gda_expr.RunSummary::merge(shards)
inspect(merged.selected_cases(), content="5")
inspect(merged.passed_cases(), content="5")
}
n 个分片中的第 i 个,取在过滤后各行中位置模 n 同余于 i 的行,因此各分片互不相交,合起来覆盖所有行。RunSummary::merge 会把计数重新累加起来。
查看解析后的行
parse_dectest 保留每一行的文本以及对其生效的上下文,因此无需执行即可检查语料:
///|
test "parsed rows" {
let source =
#|precision: 16
#|rounding: ceiling
#|maxExponent: 384
#|minExponent: -383
#|c1 add '1.5' "2" -> 3.5 -- quotes are optional
#|
let document = @gda_expr.parse_dectest("context.decTest", source).unwrap()
let row = document.cases()[0]
inspect(row.operation(), content="add")
inspect(row.operands().join(" "), content="1.5 2")
inspect(row.context().precision(), content="16")
inspect(row.context().rounding(), content="ceiling")
inspect(row.context().max_exponent(), content="384")
inspect(row.span().line(), content="5")
}
深入了解
- 交换格式行。 在精度和指数限制恰好等于 decimal32、decimal64 或 decimal128 的上下文中,以
#后接十六进制数字书写的操作数和结果是 IEEE 754 交换编码,结果按位比较。带32#、64#或128#前缀的操作数按相应格式读取。 - 预期为
?。 结果为?表示“任意值”:只比较条件。 - 自定义执行器。 每个
GdaCase都提供expression(),它返回一棵numeric_expr树,因此你可以用自己的回调来求值同样的行,例如对照另一个十进制库。 - 完整语料。
just conformance run decimal_gda会下载固定版本的官方语料,构建 native 运行器并执行所有阶段;参见验证。
常见陷阱
- 解析错误会使整个文档被拒绝。 只要有任何一行格式错误,
parse_dectest就返回全部诊断而不返回文档;请修正或删除该行。 - 默认上下文并非 decTest 的默认值。 在任何指令之前,上下文为精度 34、
half_even、指数 、extended: 1、clamp: 0。实际文件会设置各自的指令。 success()忽略被跳过的行。 如果不支持的行必须导致运行失败,请检查unsupported_cases()(或使用 CLI 的--strict-supported)。- 双写引号。 词法分析器在下一个匹配的引号处结束一个带引号的记号;decTest 格式中的
''转义会被读作两个记号。
后续步骤
- gda_expr API:每个条目。
- gda_expr 设计:精确的通过规则以及行到运算的映射。
- decimal_gda 教程:被测试的算术。