gda_expr tutorial
This tutorial shows how to run General Decimal Arithmetic test rows
(.decTest files) against decimal_gda from MoonBit code: parse a document,
execute it, read the summary, and find out why a row failed. It also shows how
to select rows and split a run into shards. To run whole corpora from the
command line, use the gda_expr_cli runner instead.
Quick start
Add the package to moon.pkg:
import {
"Luna-Flow/floating/frontend/gda_expr",
}
Parse two rows and execute them:
///|
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")
}
A row has the form id operation operand… -> expected condition…. The
directives above the rows (precision:, rounding:, …) set the context that
every following row uses.
Everyday tasks
Read why a row failed
A row passes only if both the result and the exact set of conditions (status
flags) match. Each CaseResult carries a message for a failure:
///|
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 has the right digits but omits Inexact Rounded; an extra flag is a
failure just like a missing one.
Results are compared with their exponent
The expected result is compared as a decimal representation, not only as a
number: 2.0 and 2.00 have the same value but different exponents, and only
the one with the right exponent passes. Signed zeros and NaN payloads are
compared the same way.
///|
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")
}
Skipped rows and dispositions
Some rows are not executed. Each result has a CaseDisposition:
Executable rows are run; Diagnostic rows (a # placeholder operand or
result, or a ? operand) and Unsupported rows (an unknown operation,
condition or rounding mode) are counted as skipped and never fail the run.
///|
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() only says that no executable row failed. A strict runner also
requires that nothing was unsupported; the CLI does this with
--strict-supported.
Select rows and split work into shards
RunOptions filters rows by id and selects one shard of the remaining rows.
A filter is a comma-separated list of ids or ranges first..last, where a
range matches ids of the same length that sort between the bounds:
///|
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")
}
Shard i of n takes the rows whose position among the filtered rows is
congruent to i modulo n, so the shards are disjoint and together cover
every row. RunSummary::merge adds the counts back up.
Look at the parsed rows
parse_dectest keeps each row’s text and the context in force for it, so you
can inspect a corpus without executing it:
///|
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")
}
Going further
- Interchange rows. In a context whose precision and exponent limits are
exactly those of decimal32, decimal64 or decimal128, operands and results
written as
#followed by hexadecimal digits are IEEE 754 interchange encodings, and the result is compared bit for bit. Operands prefixed32#,64#or128#are read in that format. - Expected
?. A result of?means “any value”: only the conditions are compared. - Your own executor. Every
GdaCaseexposesexpression(), anumeric_exprtree, so you can evaluate the same rows with your own callbacks, for example against another decimal library. - Whole corpora.
just conformance run decimal_gdadownloads the pinned official corpus, builds the native runner and executes all phases; see verification.
Common pitfalls
- Parse errors reject the whole document.
parse_dectestreturns all diagnostics and no document if any line is malformed; fix the line or drop it. - The default context is not a decTest default. Before any directive the
context is precision 34,
half_even, exponents ,extended: 1,clamp: 0. Real files set their own directives. success()ignores skipped rows. Checkunsupported_cases()(or use the CLI’s--strict-supported) if unsupported rows must fail the run.- Doubled quotes. The tokenizer ends a quoted token at the next matching
quote; the
''escape of the decTest format is read as two tokens.
Next steps
- gda_expr API for every item.
- gda_expr design for the exact pass rule and the row-to-operation mapping.
- decimal_gda tutorial for the arithmetic being tested.