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、指数 ±999999999\pm 999999999、extended: 1、clamp: 0。实际文件会设置各自的指令。
  • success() 忽略被跳过的行。 如果不支持的行必须导致运行失败,请检查 unsupported_cases()(或使用 CLI 的 --strict-supported)。
  • 双写引号。 词法分析器在下一个匹配的引号处结束一个带引号的记号;decTest 格式中的 '' 转义会被读作两个记号。

后续步骤