Skip to content

Add std/dec64 and std/dec64math (Douglas Crockford DEC64 decimal floating point)#25788

Open
jonasohrn wants to merge 5 commits into
nim-lang:develfrom
jonasohrn:dec64
Open

Add std/dec64 and std/dec64math (Douglas Crockford DEC64 decimal floating point)#25788
jonasohrn wants to merge 5 commits into
nim-lang:develfrom
jonasohrn:dec64

Conversation

@jonasohrn
Copy link
Copy Markdown

@jonasohrn jonasohrn commented May 2, 2026

Summary

Adds two new stdlib modules implementing Douglas Crockford's DEC64 64-bit decimal floating-point type:

  • std/dec64 — type, arithmetic, comparison, hashing, $/parse, 'dec custom literal, mixed (Dec64, int) overloads, decimal-native floor/ceil/round/trunc/abs/sign/integer-exponent pow.
  • std/dec64math — elementary transcendentals (sqrt, exp, ln/log, log2, log10, sin, cos, tan, arcsin, arccos, arctan, arctan2, sinh, cosh, tanh, pow(Dec64, Dec64), nthRoot, exact-integer factorial). Transcendentals delegate to std/math via a float64 round-trip, trading ~1 ulp at Dec64's last decimal digit for a pure-Nim implementation that wraps libm rather than reimplementing polynomials. A precision-preserving dec64FromFloatPrecise is exposed alongside the fast scale-and-round dec64FromFloat for callers who need shortest-round-trip fidelity.

Small footprint: ~510 lines of executable Nim across both modules, plus ~210 lines of comments. Tests are another ~350 lines.

Relation to RFC #308

This PR is a partial response to nim-lang/RFCs#308 ("Add decimal to standard library", Accepted RFC). That RFC primarily discusses IEEE 754-2008 decimal128 (128-bit, 34 digits) as the target format.
This PR instead implements Crockford's DEC64 (64-bit, ~16.8 digits) — a smaller, faster alternative aimed at money and human-facing measurements where DEC64's range and precision are sufficient.
An IEEE 754 decimal128 implementation remains a valuable follow-up and could coexist as e.g. std/decimal128; the two address different precision/size tradeoffs.

Why DEC64 in stdlib (not just Nimble)

DEC64 is a foundational numeric type — money, measurements, human-facing decimal values where binary float rounding is a bug. As a stdlib module it composes with tables, json, strutils, etc. without a Nimble dependency.

Test plan

  • nim c -r tests/stdlib/tdec64.nim (orc + refc)
  • nim c -r tests/stdlib/tdec64math.nim (orc + refc)
  • nim c -r -d:release tests/stdlib/tdec64math.nim
  • nim doc lib/pure/dec64.nim and lib/pure/dec64math.nim — runnableExamples pass
  • dec64FromFloat (fast) and dec64FromFloatPrecise (string round-trip) agree to within 2e-15 relative on inexact inputs

Open to feedback on

  • Dec64 vs DEC64 for the type name — currently Dec64 per Nim's PascalCase convention; the spec uses both spellings.
  • Whether dec64FromFloatPrecise belongs in the public API. It duplicates dec64.nim's dec64(f: float) constructor exactly; could be removed in favor of pointing precision-conscious callers at the constructor.
  • Hyperbolic functions (sinh/cosh/tanh) — included for symmetry with std/math; happy to drop if reviewers prefer a tighter surface.
  • Could add Table & Set providing Dec64Table[V] and Dec64Set that canonicalize keys at the boundary, with the canonical invariant enforced by a private distinct tag so the stdlib Table/HashSet internals see only fast bit-compare hash/==.

Tests cover packing, NaN propagation, alignment overflow, the 128-bit multiplication path, division precision, parse/$ round-trip, hash equality after canonicalization, native-vs-float-trip parity, and std/math agreement to 1e-14 relative on a sampled domain.

jonasohrn and others added 4 commits May 2, 2026 23:39
std/dec64 implements Douglas Crockford's DEC64
(https://www.crockford.com/dec64.html) as a `distinct int64`: 56-bit
signed coefficient, 8-bit signed exponent, NaN at exponent -128.
Provides arithmetic (+, -, *, /, unary -), comparison
(==, <, <=, cmp, hash), parsing/$ round-trip, a `'dec` custom literal,
mixed (Dec64, int) operator overloads, and decimal-native
floor/ceil/round/trunc/abs/sign and integer-exponent pow that stay in
pure decimal throughout.

The equal-zero-exponent fast path for `+` embeds Crockford's verbatim
x64 / ARM64 inline-assembly snippets via `{.emit.}`, gated on
amd64+gcc/clang and arm64+gcc/clang. A pure-Nim equivalent runs on
every other backend including JS. `-d:nimDec64NoAsm` forces the
pure-Nim path everywhere, which the test suite uses to verify both
paths produce identical results.

std/dec64math provides the elementary transcendentals: sqrt, exp,
ln/log, log2, log10, sin, cos, tan, arcsin, arccos, arctan, arctan2,
sinh, cosh, tanh, pow(Dec64, Dec64), nthRoot, and exact-integer
factorial. Transcendentals delegate to std/math via a float64
round-trip, trading ~1 ulp at Dec64's last decimal digit for a pure-
Nim implementation that wraps the battle-tested libm rather than
reimplementing polynomials. A precision-preserving variant
`dec64FromFloatPrecise` is exposed alongside the fast scale-and-round
`dec64FromFloat` for callers who need shortest-round-trip fidelity.

Tests cover packing, NaN propagation, alignment overflow, the 128-bit
multiplication path, division precision, parse/$ round-trip, hash
equality after canonicalization, the inline-asm and pure-Nim paths
producing equal results, native-vs-float-trip parity, and std/math
agreement to 1e-14 relative on a sampled domain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline asm across {x64, arm64} x {gcc, clang} x {C, C++} isn't worth
the maintenance burden for shaving a few cycles off the equal-zero-
exponent `+` case. Apple's clang++ on darwin-arm64 rejects the `"x2"`
clobber, breaking CI; the pure-Nim path covers it. Also folded
`addImpl` into `+` so there's a single execution path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the textual output with std/formatfloat. Dec64 values now
print identically to their float64 equivalent (e.g. dec64(8, -8)
becomes "8e-8" rather than "0.00000008").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Construction now packs verbatim — trailing zeros stay, so 1.5 + 1.5 prints
"3.0" and 1.25 * 4 prints "5.00". `==` uses Crockford's subtract-then-check
to keep value equality across encodings; `hash` canonicalises lazily to
stay consistent. Multiplication switches to nearest-even rounding. Add
`significantDigits` / `fractionalDigits` to introspect carried precision.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant