Changelog¶
All notable changes to decimal-scaled are documented here.
The format is loosely based on Keep a Changelog, and this project follows Semantic Versioning.
[0.5.1] — 2026-09-02¶
An API release, non-breaking for callers. Operators now work between
decimals of different storage width and different SCALE; a new
requantize moves both the width and the scale axis in one call; and the
scale-only operation takes the name the decimal arithmetic specification
uses for it — quantize. The former rescale spellings remain as
deprecated aliases. Two functions join the transcendental surface,
log1p and expm1, both correctly rounded at every width and scale, and
the two remaining rounding modes of the decimal arithmetic specification
join RoundingMode, completing it against that specification at eight.
Code that implements DynDecimal or DecimalTranscendental outside the
crate does need an update — see Breaking — for trait implementors only
at the end of this section.
Added¶
- Cross-width and cross-scale operators.
Add,Sub,Mul,Div,Remand their compound-assignment forms are implemented between every pair of decimal widths, at any pair ofSCALEs. The result takes the wider storage and the left operand's scale, so the type ofa + bis fixed and predictable; a declared output type the operator cannot produce is a compile error rather than a silently different scale. Narrowing compound assignment (narrow op= wide) computes at the wider width and panics on overflow, matching the crate's existing overflow contract. requantize/requantize_with— change storage width andSCALEin a single call, in either direction, to any width and any scale. The target is inferred from the binding (let r: D38<6> = a.requantize();), so a call site never spells a limb count. The two steps are ordered by direction — widen before a scale-up, scale down before narrowing — so a value the target width can hold does not overflow on the way there. Overflow panics with the crate's standard wording.- Public
log1p.log1p(t) = ln(1 + t), domaint > -1, correctly rounded at every width and scale, with the full surface —log1p_strict,log1p_strict_with,log1p_approx,log1p_approx_with, the_fastpair, andDecimalTranscendentalentries. Provided for API parity and standards conformance: at fixed point it is equivalent toln(1 + t)at the same scale, not more accurate —1 + tis exactly representable, so there is no cancellation to avoid, unlike binary floating point. - Public
expm1.expm1(x) = e^x - 1, total over the argument, correctly rounded at every width and scale, with the same full surface —expm1_strict,expm1_strict_with,expm1_approx,expm1_approx_with, the_fastpair, andDecimalTranscendentalentries.
Unlike log1p, this one is not merely parity. Trunc does not commute
with the - 1, so on the negative half exp(x) - 1 is a genuinely wrong
answer rather than a slower one: Trunc rounds toward zero, and for
x < 0 the subtraction crosses zero — e^x lies in (0, 1) and has its
magnitude rounded down, while expm1(x) < 0 is rounded up — leaving
the two-step form one ULP out. Floor, Ceiling and the nearest modes
commute, since they are translations by a grid point; Trunc does not.
It also reaches slightly further than exp: the - 1 is applied at the
working scale, ahead of the range check, so the domain is
x <= ln(1 + MAX) against exp's x <= ln(MAX) — a band ln(1 + 1/MAX)
wide, a few hundredths of an argument unit.
checked_expm1_strictandchecked_log1p_strict— theOption-returning siblings, so the two new functions match the surface every other strict transcendental carries.
log1p's is the one that matters. Its domain genuinely ends at
t > -1 and the strict form panics below that, so until now a caller who
could not guarantee the domain in advance had no non-panicking route to the
function at all. The checked form returns None exactly at that wall and a
bit-identical Some everywhere else — and it is a complete guarantee, not
a partial one, because no out-of-range case exists: ln(1 + t) is bounded by
the storage range wherever 1 + t is representable.
expm1's completes the surface without adding a guarantee. It is total
over its argument, so there is no domain wall to check, and its overflow seam
is not threaded — an out-of-range expm1 still panics, and the checked form
returns Some whenever the default returns at all. That is the same gap the
existing wide-tier notes record for ln, log and exp; threading it is
separate work.
-
The two missing GDA rounding modes —
AwayFromZeroandZeroFiveUp. The crate implemented six of the decimal arithmetic specification's eight rounding modes; these are the other two, soRoundingModeis now complete against that specification. Both are accepted everywhere a mode is — every*_withmethod, the quantize surface, and the crate-wide default feature flags — and both are covered by the golden gate at every band-edge(width, scale)cell. -
AwayFromZero(GDAround-up) rounds away from zero whenever anything was discarded, the exact mirror ofTrunc.0.1 -> 1,-0.1 -> -1, and an exact value is never moved. ZeroFiveUp(GDAround-05up) rounds away from zero iff the last retained digit of the toward-zero result is0or5, and truncates otherwise — so0.7 -> 1and5.7 -> 6, but1.7 -> 1and4.7 -> 4. It is indifferent to how much was discarded, which is the point: reserving0and5as the only final digits that can absorb a remainder means a later round to one fewer digit never meets a half-way tie that the first rounding manufactured. This is the "round for reround" rule, and it is the reason the mode looks arbitrary in isolation.
The golden surface consequently grew from six modes to eight — a third more graded rows per cell, at 96,159,960 checks.
Changed¶
rescaleis nowquantize. The scale-only operation is renamed toquantize/quantize_withon every decimal width, andwith_scaledelegates to it. This is the name the decimal arithmetic specification uses; that specification marksrescaleas its deprecated spelling. Behaviour is unchanged — the rounding logic is byte-identical, only the name moved.DynDecimal::rescale_tois nowquantize_to. The runtime-polymorphic facade follows the same rename, asquantize_to/quantize_to_with. These are the trait's required methods; the former names remain as deprecated provided methods that delegate to them. Callers are unaffected — but code implementingDynDecimaloutside the crate now implements the new names.-
The scale-up overflow panic message names
quantizerather thanrescale. -
The golden oracle fetched values by rounding instead of truncating, and six
expanswers were wrong because of it. This affectsdecimal-scaled-golden— how every golden answer is produced — so it is worth reading even if you only consume the published set.
An oracle value is defined as the true value truncated toward zero to
the generation depth plus two guard digits. The decimal adapter instead
computed at a working precision under half-even rounding and truncated only
afterwards. Where a value sits just under a storage grid line its digits run
9 from the stored depth downward, and when the working precision landed
inside that run the rounding carried up past the stored depth: …4|99
became …5|00. Both guard digits are destroyed at once — and the same carry
manufactures the run of zeros that the termination check reads as "this
value terminated", so a transcendental value was simultaneously recorded as
exact. The corrected answers are for exp at -1e-306, -1e-461,
-1e-462, -1e-615, -1e-616 and -3e-280.
Three consequences, all now closed:
- One fetch contract, shared by every oracle. Fetch to depth + guard,
truncated toward zero; an exact value that terminates shorter keeps its
own shorter length.
decimal,mpmathandflinthad carried byte-identical copies of this logic andmpfr/sympya stripped one with no guard window at all — six copies of one algorithm, which is what let a generator and one of its validators fail identically and read as agreement. Each adapter now supplies only a scaled floor in its own numeric type; everything after that is shared. flint/Arb generates. Only rigorous intervals can pin a truncation; a point value behind a fixed window can merely approximate one, and that limitation is exactly what produced the wrong rows. The per-function generator table is gone. (remstays onfraction, which is exact — the flint adapter implements norem.)-
A validator confirms within one unit at the last guard digit, down from two. All oracles floor the same value at the same depth, so the only honest difference is an internal error straddling the truncation boundary, worth at most one unit — the bound now follows from the contract rather than from observed noise.
flinthad in fact dissented by exactly one unit on all six rows from the day they were generated; the old tolerance accepted it, and the annotation blamed it on binary-vs-decimal radix. Line comments now record which validator differed and by how much, and no longer offer a cause. -
DynDecimalnow formats without allocating. The trait requirescore::fmt::Display, so{}works directly on adyn DynDecimaland the caller chooses whether to allocate.
Fixed¶
lnreturned the nearest-mode answer underCeilingnearx = 1(present since 0.5.0). At D616<590>, D616<615> and D924<693>,ln_strict_with(Ceiling)gave theHalfToEvenresult. The near-one guard tested only the tangent bracket — whether the result equals the linear termδ = x - 1. For arguments whereδ²/2happens to be an exact multiple of one ULP, the value instead lands on the parabola pointδ - δ²/2, a different grid point, so the guard did not fire; the deciding cubic then sits at depth ~3·SCALE/2, past the Ziv walker's reach, and the kernel treats the value as exactly on the grid.
It is one-sided for a reason worth recording: since
d/du [ln(1+u) - u + u²/2] = u²/(1+u) is strictly positive and vanishes
at u = 0, the true value lies strictly above the parabola for
x > 1 and strictly below it for x < 1. So a value landing on the
parabola leaves Floor, Trunc and the nearest modes accidentally
correct, and only Ceiling wrong.
Both brackets now live in one generic kernel, and the test is an exact
integer comparison evaluated at double width — δ² genuinely overflows
a single width at high scales, so a single-width check would pass the
cells that exposed the bug and fail wider members of the same family.
Not fixed by raising the walker cap.
- log1p sub-resolution directed rounding near t = 0. For t at one
LSB the deficit t²/2 falls below the Ziv precision horizon, so the walker
saw a zero residual and Floor returned one LSB high (reproduced at
D1232<1231>). Resolved analytically, mirroring how the Tang ln path
handles the same situation near 1 — not by raising the walker cap.
log1p carried the same too-narrow tangent-only guard as ln above,
so the parabola case reached it too; both are now faces over the shared
kernel and were fixed together.
- log1p directed rounding in the artanh band. Separate from the
near-zero case above, and larger: 52 rows across five cells returned the
grid point under Floor and Trunc where the true value sits strictly
below it.
The guard that should have caught them could not. It tagged the result's
side only when the working value was exact, and exactness required u²
to be exact — but u is a rounded quotient whose square does not divide
10^w, so a single contributing series term was enough to defeat it. The
guard could therefore only ever fire when the artanh series contributed
no terms at all. Every failing argument arrived untagged for that
reason, not because a side had been computed wrongly.
Two stages replace it. Where each rounding's direction — not merely its
exactness — is unanimous and agrees with the neglected tail, no cancellation
between error contributions is possible and the side is proved. Unanimity is
the precondition of that proof rather than a convenient observation: one
opposing term makes the answer depend on magnitudes, which no argument over
signs can settle, so mixed directions fail closed. Where the directions
genuinely do oppose, the side is measured — the series is re-evaluated
deeper — instead of guessed or refused, at a depth derived from the width's
BITS and never from an argument or a cell.
The deeper evaluation runs only where its answer can be consulted. The walker reads the tag at an exactly-zero residual and at an exact half, and nowhere else — everywhere else the residual decides and the sign is discarded unread — so the probe is skipped wherever it would be thrown away. Verified as a pure laziness change by identical checksums over 6018 results, which differ against a build with no probe at all, confirming the mechanism is not merely inert.
As with ln above, not fixed by raising the Ziv walker's cap: that cap is
co-designed with the generated constant tables, and lifting it lets a kernel
request a table entry that does not exist — a panic in narrow builds,
surfacing far from the change. artanh needs only 10^w and touches no
per-scale table, which is why the deeper evaluation is available to it.
- expm1 mis-rounded at the wide tiers, in the directed modes and in
HalfToEven. The series can land exactly on a storage grid point,
because x^j / j! is exact whenever the argument supplies the odd primes
in j!. The walker then read a zero residual, took the value to be
exactly representable, and returned that grid point in every mode — so
whichever mode needed to step off it got its neighbour's answer instead.
The kernel now reports the neglected tail's side — above or below — for each probe, and the strict walker rounds from that rather than inferring it from the residual. The side is claimed only where it can be proved; anywhere it cannot, the walker keeps its ordinary escalation, so an unprovable case fails closed rather than guessing.
What must be proved is that the accumulated error is exactly zero,
not that each term is individually exact. Per-term exactness is
sufficient but not necessary, and the difference is not academic: the
last remaining failures were arguments where the third and fourth terms'
error contributions cancel exactly (+2/3 and -2/3), leaving the sum
exact though neither division is. The error is carried as an exact
rational, and every case it cannot represent reports "unproved".
This rule is available to expm1 only because its series is seeded with
the exact input rather than a quotient. log1p opens with a divide that
is provably never exact for the affected family, so it needed the
different rule described above.
- sin and cos mis-rounded at tiny arguments with more than one
significant digit (present since 0.5.0). The directed adjust for the
tiny-x band named the deciding term with a digit-count formula derived
from the argument's exponent. That formula is blind to the significand:
it is correct for a single-digit argument and wrong as soon as the
argument has more digits than one.
At x = 3e-153 + 1e-252 and SCALE = 461, the cubic term carries
450 + 4.5e-97. The 4.5e-97 is sub-LSB imprecision — below the last
stored digit, so the walker cannot see it, but 207 digits shallower
than the term the formula names, so the formula does not account for it
either. It falls in the gap between the two. Worse, its sign is the sign
of the significand's cube, so it flips with the argument's last
significant digit: 3e-153 - 1e-252 has the same exponent, the same
term index, and the opposite correct answer.
The replacement carries no closed-form claim at all. Consecutive partial
sums of an alternating series with strictly decreasing terms straddle the
true value, so the pair brackets the answer without anyone having to say
how deep the deciding digit sits — there is no depth claim left to be
wrong about. One generic kernel serves sin, cos, atan and asinh,
each supplying its own term-ratio recurrence. tan and asin are
deliberately excluded: their Taylor coefficients are all positive, so
consecutive partial sums approach from one side instead of straddling and
the bracket's precondition genuinely fails; they keep the existing path,
where all-positive coefficients make the sign unconditional.
It went unfound for a release because every adversarial input tried against this band had a single-digit significand, and reproducing it needs roughly 47 significant digits. The golden lead now carries the multi-digit family.
atan2 joins the same kernel. It could not simply reuse the storage face —
its call sites pass the result as the argument, so a test posed on the
computed value reasons in a circle. Posing the bracket on the exact rational
y/x removes the circularity: the 10^SCALE cancels, so the two setup
quantities are integer divisions that keep numerator and denominator paired
and never form the ratio itself. No behaviour is expected to change there —
the bracket agrees with the previous parity rule wherever parity was right,
and can differ only where parity was wrong, which needs a tiny argument
whose y/x carries a multi-digit significand landing on a grid point. The
value is that the path is now proved rather than parity-dependent.
The bracket's own error bound is carried exactly rather than assumed. An
earlier form counted only the two truncations per step and missed that the
dominant loss is amplified by the term it multiplies — bounded by that
term's own magnitude, which on the first step is the argument itself. The
answers it produced were correct, but on a relation between the work width
and the scale that nothing checked. The bound is now computed and tested at
run time, and fails closed rather than trusting the width table.
- exp mis-rounded negative arguments in the directed modes
(present since 0.5.0). The wide path reached the Ziv walker's unresolved
endgame under a blanket assertion — that the sub-resolution tail always
moves the magnitude away from zero. That is sound for x > 0, where
every Taylor term is positive, and backwards for x < 0, where the series
alternates. 48 rows across D924<900, 923> and D1232<924, 1200, 1231>,
identical under Ceiling, Floor and Trunc.
It is the same shape as the four above — a claim about which side the neglected tail falls on, asserted rather than proved — and it was the hardest of them to see, because the grading harness had made the same assumption independently: a zero residual at full precision was mapped to a positive hidden tail, which is bit-for-bit the verdict the kernel produced, so oracle and defect agreed and the gate stayed green.
The walker's endgame now reads a per-probe side, and the kernel supplies
one only where it can be proved. try_exp_fixed cannot: it runs a fixed
number of rounded divides keyed on the working scale — 61 at D1232<1231>,
paid even by exp(-1e-430) — each up to half a working unit with its
direction untracked, while the tail being reported on is sub-unit, so any
side from that path would be another assertion. Where the direct series
pays for itself, exp is instead evaluated as 1 + expm1(v): the 1 is
10^w exactly, so the addition is exact and the side transfers unchanged
from expm1's rule, which already fails closed. Everywhere else the value
is bit-identical to before. The choice between them is a cost gate, not a
validity wall — both kernels are correct at every argument it sees, so a
mis-estimate costs speed and never accuracy.
- asinh panicked at D924 and D1232 — a regression this branch
introduced and fixed before release, not a defect in 0.5.0. The tiny-x
bracket added above divided through the width-erased slice engines, whose
normalisation scratch is sized from a build-max constant derived from the
storage-scaled work widths. That constant never accounted for the AGM
work integer, and the asinh face instantiates the bracket at exactly
that integer — Int<192> at D924, Int<256> at D1232 — so the dividend
ran off the end of a 258-limb buffer. 5880 panics across 33 cells, no
wrong values.
Every divide in the kernel now takes the divide matcher's own verdict and
calls the chosen engine's caller-supplied-scratch door, sized exactly from
the width in hand — the pattern the reciprocal and widening-divide paths
already use. Worth recording rather than quietly repairing, for two
reasons: a per-diff review had passed the offending call, and it was the
full-surface gate that caught it, which is the argument for running that
gate on every merge rather than on the ones that look risky.
- FromStr rejected exactly-representable trailing zeros. "1.00" at
SCALE = 0 returned ParseError::OverlongFractional, as did "2.0",
"-1.0", "0.0" and every literal whose digits past SCALE are all
zeros — although each is exact and round-trips losing nothing. The check
counted raw fractional characters where it should count significant
ones; excess zeros are now trimmed before the width check. This matches
the representability rule the golden harness already applies and the
dec! macro's existing all-zeros test. Genuine precision loss is still
rejected — "1.05" and "1.050" at SCALE = 0, "1.55" at SCALE = 1.
- Golden validator: an unsound oracle could veto a sound vector. A
validator that cannot represent an input abstains harmlessly, but one that
computes a wrong value drops the whole line — so it could discard a vector
that the reliable oracles agreed on, leaving a silent hole in coverage
exactly at the hardest inputs. It was the mpmath validator doing this near
log1p's t = -1 pole, atanh's endpoints and acosh at 1 — but the
cause was ours, not the library's: this repo's mpmath adapter budgeted
working precision from the result's magnitude with no term for the
condition number, so at 1 + t = 1e-70 it sized against
ln(1e-70) ~= -161 — three integer digits — while the derivative
1 / (1 + t) destroyed seventy. The adapter now sizes by the worse of the
result's magnitude and A = log10 |x . grad f(x)|, the digits the function
itself destroys; the same latent defect affected sin/cos/tan of a
large argument, where A = log10|x| is exactly the range-reduction cost.
A per-function VALIDATOR_EXCLUDE facility remains for a genuinely
non-viable validator, but it is now empty.
(Oracle-only — this is the test harness that grades the crate, not shipped
code. No golden value changed: all 4149 atanh and 4302 acosh committed
lines revalidate identically.)
Internal¶
absmoved onto theBigInttrait, replacing four copies of the same three-line function — two free functions in separate algorithm modules, and one emitted per tier by a macro, so the per-tier copies scaled with the width list rather than staying at one. No behaviour changes; the trait method is what every caller now reaches.- The
expm1andlog1ppublic-surface tests moved into thedecimal-scale-testcrate, where the rest of the public-API coverage lives. They had been the onlytests.rsfiles undersrc/algos/, a third layout no other module used. The few cases that genuinely need crate-internal items stay behind as inline#[cfg(test)]blocks, the convention the rest of the tree follows. - Every baked constant is now derived from a rigorous flint/Arb oracle.
The generators previously took their digits from
mpmath, which carries no error bound: it computes to a working precision and reports what it has, so the last few digits of a long constant are not guaranteed. The constant and Tang-table generators now use flint's Arb intervals and emit a digit only where the enclosure pins it — a value that cannot be pinned escalates precision rather than being reported. Regeneration also turned out to be 5× faster (9.8s → 2.0s) at a higher working precision, because Arb is a C library andmpmathis pure Python. flint is a generator-side and CI dependency only; nothing from it ships, and the crate's licensing is unchanged. - A generator that no longer reproduces its output now fails the build.
Two committed tables had silently diverged from the generators that
produce them. The mechanism is worth recording because it is structural
rather than careless: adding an enum variant breaks every exhaustive
matchimmediately and loudly, with a file and a line number, so the fix lands in the file the compiler pointed at — which, for a generated table, is the output rather than the generator. The "do not edit by hand" header is a comment; the broken build is not. CI now re-runs every generator and fails on any working-tree change (about 14s, behind a paths filter), and the headers name the generator as where a match arm belongs. - Parameters and locals named across the tree. A sweep over 190 files
replaced single-letter and abbreviated bindings with names drawn from the
tree's own documentation. It corrected names that were actively
misleading rather than merely terse — an
r256holding a 128-bit pair, ahiused as a length beside ahiused as a value, ana2/b2pair meaning opposite things within one file, and a Newton iteration binding a limb toswhere the surrounding derivation definessas the root. Rust has no named arguments, so parameter names are file-local and no call site changed; the work is entirely legibility. - Golden striping sized from measurement. The comprehensive gate's fan-out is now derived from measured per-stripe durations rather than from tier width, which had over-provisioned the middle tiers while leaving the widest one setting the critical path alone. Same total graded rows, 85 jobs rather than 86, and roughly half the wall time.
- Dependencies.
decimal_scaled_macrosmoves tosyn3 — the only dependency change that reaches a published crate. The remaining bumps (dashu-int/dashu-floatto 0.6,decimal-rsto 0.2,g_mathto 0.5) are confined to the benchmark and golden-comparison members, which arepublish = falseand reach no consumer.
Performance — findings¶
- Most of the timed surface is slower than 0.5.0, by single-digit
percentages in the median. Of 1620 measured cells, 773 clear the
assessability floor (over 1% and over 10ns, below which the shared
runner cannot distinguish a change from jitter); of those, 611 regressed
and 162 improved.
lnis the most affected op — 49 cells, median +8.5%, worst +18.1% at D616<462> — followed byto_degrees/to_radiansaround D115. The worst single cell isdivat D57<42>, +23.4%. The spread covers nearly every op and every width from D38 up, so it is not one kernel.
Acknowledged rather than fixed in this release. The correctness work
above added a proved tail side to paths that previously asserted one,
and the two new rounding modes widened the single decision point every
kernel funnels through; the alternative was to keep a directed-rounding
defect to protect a benchmark. Tracked as issue #114, with the measured
table — the mechanism is not yet traced, and the numbers there are one
sampled (operand, scale) point per cell rather than a tier's whole
range, so it is a starting record and not a diagnosis.
- The wide-tier transcendentals got materially faster at scale 0.
Every one of the fifteen largest improvements sits there, and they are
larger than the largest regressions:
tanat D230<0> −28.5%,cosat D462<0> −24.0%,expat D462<0> −23.4%, withsin,sinh,coshandexpin the same band across D230 through D616. What produced them has not been traced either, and whether it can be extended off scale 0 is the same open question as the regression above.
Deprecated¶
rescale/rescale_withandDynDecimal::rescale_to/rescale_to_with— renamed as above. They delegate unchanged and are removed in 0.6.0.DynDecimal::display— the trait now requiresDisplay, so use{}formatting instead.display()always allocates aString; the deprecated method delegates unchanged and is removed in 0.6.0.
Breaking — for trait implementors only¶
Callers are unaffected: every rename keeps a working deprecated alias, and
everything else is additive. But DynDecimal and DecimalTranscendental are
deliberately not sealed — they are extension points, and implementing them
from outside the crate is the intent — so the following require an update to
an external impl:
DynDecimalgained aDisplaysupertrait. An external implementor must also provideDisplay.DynDecimal::quantize_to/quantize_to_withare the required methods;rescale_to/rescale_to_withbecame deprecated provided methods.DecimalTranscendentalgained four requiredlog1p_*methods, and four requiredexpm1_*methods. Neither could sensibly be a provided method: a default body would have to compose the result fromln/exp, and forexpm1that composition is demonstrably wrong underTruncon the negative half (see Added). A default that is incorrect for one rounding mode is worse than a required method.
Accepted deliberately at this point in the pre-1.0 series, where Cargo treats
all 0.5.x as compatible. Future additions to either trait should prefer
provided methods with default bodies, so an implementor is not forced to
update every time a function is added.
[0.5.0] — 2026-06-14¶
An architecture release. Every decimal width is now backed by the
const-generic Int<N> / Uint<N> integer layer introduced in this
cycle, unifying storage across the full tier table and eliminating the
native (primitive-integer) backend entirely. The 32-bit tier D9 is
removed. The integer layer itself gains a parallel types / policy /
algos / limbs layout mirroring the decimal layer, plus a suite of
performance and correctness fixes that fed directly into the decimal
migration. The crate-wide default rounding mode features, docs
single-source tooling, precision harness, and pre-merge CI gates all
land alongside.
Removed¶
D9(32-bit tier) removed in full. TheD9<SCALE>type, thed9!construction macro (bothconstandfnforms), thewtag::D9tag constant, thedynregistry entry, and all corresponding documentation and prose are gone.D18is now the narrowest supported width. Callers usingD9should migrate toD18; the operator surface, rounding, and conversion APIs are identical.- Native decimal backend removed. The non-wide macro arms that
dispatched arithmetic, display, sign, conversions, equalities, and
num_traitsthrough primitive-integer storage are deleted. Every decimal tier now routes through the unifiedInt<N>wide arms. The deadround_with_mode_nativehelper and thei64-widening arithmetic machinery (@native_i64_wider,i64dispatch arm) that served onlyD9are gone. Unsignedtrait removed. The internal marker trait carried no live impls after the native backend was dropped.- CodSpeed perf-tracking removed. The
codspeed.ymlworkflow and thecodspeed-criterion-compatbench alias are gone; the benches now build directly against the upstreamcriterioncrate, and perf is tracked through thebench-branch-compareworkflow.
Changed¶
D18storage changed fromi64toInt<1>. The 64-bit tier now stores its scaled integer inInt<1>(a one-limb const-generic integer) instead of a barei64.to_bitsandfrom_bitsnow accept and returnInt<1>where they previously usedi64. This is a breaking type change for call sites that stored or pattern-matched the raw bits directly.D38storage changed fromi128toInt<2>. The 128-bit tier is now backed byInt<2>(two 64-bit limbs).to_bitsandfrom_bitsnow useInt<2>instead ofi128. Existing call sites that bridged throughi128should construct viaInt::<2>::from(v)/Int::<2>::try_from(v)?and read back withi128::from(int2)(theFrom/TryFromtrait surface). This is a breaking type change.D18narrowing conversions tightened.TryFrom<i128>andTryFrom<u128>forD18now reject values outside thei64storage range (previously the wide arms assumed ≥ 128-bit storage and silently truncated).FromPrimitive::from_u64on narrowInt<1>storage likewise rejects out-of-range inputs.checked_div/wrapping_div/overflowing_div/saturating_divround to nearest like the/operator. These four methods previously performed a truncating integer divide of the scaled numerator, disagreeing with the rounding/operator (e.g.20/3at SCALE 2 returned666instead of667). They now route through the sameround_with_mode_widestep that/uses. This fixes a long-standing divergence on the wide tiers.Int<N>/Uint<N>are now the public storage types. All shipped widths (D18 through D1232) exposeInt<N>as theirStoragetype.Int<N>/Uint<N>are re-exported from the crate root, anddecl_wide_int!is removed.- BREAKING: the named-width integer aliases are removed. The
crate-root
decimal_scaled::{Int64, Int128, Int192, …, Int16384}(and the matchingUint*) re-exports are gone — name the storage as the const-genericInt<N>/Uint<N>instead (e.g.Int<4>for the formerInt256). - BREAKING: integer → decimal construction is now
TryFrom, notFrom. Every primitive-integer source (i8…i64,u8…u64,i128,u128) builds a decimal viaTryFrom, returningConvertError::Overflowwhenvalue * 10^SCALEexceeds the storage:let d = D38::<2>::try_from(20i64)?;. Scaling can overflow even for small inputs near a width's top scale, so the old infallibleFrom<iN>impls (which silently wrapped on overflow) are removed, along with theDecimalConvert::from_i32trait method and the inherentfrom_int/from_i32constructors. - Uniform overflow & domain contract — strict ops panic by
default. A strict transcendental panics on a domain error
(
lnof a non-positive value,asinoutside[-1, 1], …) or an out-of-range correctly-rounded result, in both debug and release, independent of tier and scale — the condition is detected once and a wrapper applies the policy. The newchecked_*siblings (see Added) opt intoOptionreturns instead of a panic. - Published crate slimmed to a source allowlist. The package now
ships via a
[package] includeallowlist (src/**plus the README / CHANGELOG / CONTRIBUTORS / LICENSE files) rather than the whole repository, excluding the benchmarks, golden harness, dev docs, and tooling from the published.crate.
Fixed¶
saturating_divwith a zero divisor now panics (matchingstd). Previously it saturated toMAX, silently returning a nonsense value where every other integer type in Rust panics in debug.checked_rem/overflowing_remhonourMIN % -1overflow.checked_rem(MIN, -1)now returnsNone;overflowing_rem(MIN, -1)returns(ZERO, true), matching the primitive integer contract. Both tests that covered this case were un-ignored.dynfeature compiles after theInt<N>migration. Thedyn_bridgemacro previously assumed primitive storage (10 as $Storage); it now constructs the rescale multiplier viaInt::from_i128(10).pow(shift)and converts to theRawStoragevariant primitive withas_i128().D18fits thei64variant,D38fits thei128variant exactly.Int<N>arithmetic sign-preserving right-shift restored.Shrwas not sign-extending correctly for negative values, corrupting wide-tier transcendental range-reduction. Fixed; confirmed against the 264-cell golden suite.Int<N>::leading_zerosis two's-complement-faithful. Negative values now correctly return0(all bits set under negation), matchingiN::leading_zerosparity. The wideMul/Divfast-path now takes magnitude viaunsigned_absrather than callingleading_zeroson signed values.D38doc examples use the publicdecimal_scaled::Intpath. Examples that usedcrate::intfailed in downstream doctests; corrected to the external-crate path.- Intra-doc link warnings resolved under
-D warnings. Broken links surfaced by the newcargo doc --no-deps -D warningsgate are fixed.cargo docis now clean under-D warnings. D18narrowingTryFrom<u128>rejects values past signed storage range. Wide arms previously assumed ≥ 128-bit storage and truncated silently.Int<1>is the first narrow wide-storage tier; the guard is a no-op for wider tiers.- Correctly rounded across all six rounding modes on the wide
tiers. A family of directed-rounding fixes closes the last
sub-0.5-ULP gaps the six-mode golden surfaced: the tiny-argument
directed wrong-mode in the wide-tier trig /
atan2/asinhpaths, theexpdeep-underflow directed-rounding inversion, apowf(2, -200)-class deep-underflow mis-round at the D57 / D76 mid-scales, and a large-argument panic in the wide hyperbolics. Every shipped width is now0 (0)— correctly rounded — under all six modes (HalfToEven/HalfAwayFromZero/HalfTowardZero/Floor/Ceiling/Trunc).
Added¶
Int<N>/Uint<N>const-generic integer layer. A const-generic fixed-width signed / unsigned integer pair parameterised by 64-bit limb count (N) backs every decimal tier.Int<1>replacesi64;Int<2>replacesi128; wider tiers map toInt<3>throughInt<64>. The layer is mirrored undersrc/int/with types / policy / algos / limbs sub-modules matching the decimal layer's six-bucket layout.BigInttrait — a single unified trait surface forInt<N>, providing the full method coverage (LIMBS,BITS,ZERO,ONE,MAX,MIN,leading_zeros,wrapping_add,wrapping_sub,wrapping_mul,div_rem,isqrt,widen,narrow, …).Int<N>/Uint<N>std-aligned primitive conversion surface. The crate-internalfrom_i128/as_i128helpers are nowpub(crate); the public conversions go through the idiomaticFrom/TryFromtraits:From<i8>/From<i16>/From<i32>/From<i64>forInt<N>— infallible widening from a narrow signed primitive.From<u8>/From<u16>/From<u32>forInt<N>, andFrom<u8>/From<u16>/From<u32>/From<u64>forUint<N>— infallible widening from a narrow unsigned primitive.TryFrom<u64>forInt<N>(au64can overflowInt<1>),TryFrom<i128>forInt<N>, andTryFrom<u128>for bothInt<N>andUint<N>— fallible narrowing, returningConvertError::Overflowwhen the value is out of range.From<Int<1>>/From<Int<2>>fori64/i128and the matchingPartialEq/PartialOrdbridges, so the one- and two-limb tiers interoperate with the corresponding primitive.- Non-allocating stack-scratch Karatsuba multiply in
wide_int::widen_mul. The non-alloc Karatsuba dispatcher is wired intowiden_mul; the schoolbook path remains optimal at all currently-shipped widths (crossover would require limb counts beyondInt<64>), so the threshold is parked for GHA bench validation. div-by-10^19base case for wide-intto_string. Theto_stringimplementation now peels 19-digit chunks by dividing by10^19(the largest power-of-ten below2^64), emitting each chunk with native arithmetic and calling the expensive full-width divide once per 19 digits. Measured speedup: 2.3× (Int256/ D76) to 14.9× (Int4096/ D1232).mg_divideclean-room rewrite from the Möller–Granlund 2011 paper.MG_EXP_MAGICSis now generated at compile time by aconst fn(mg_reciprocal) that computes the magic constant via binary long division, following the paper's reciprocal formula. No literal table values are copied from any prior implementation. The function namesmul_u128_to_u256anddivmod_pow10_2wordreplace the prior upstream-derived identifiers.LICENSES/THIRD-PARTY.mdconfirms no third-party code is incorporated;ALGORITHMS.mdcarries the clean-room declaration and a courtesy prior-art note.no_std+allocbuild restored and CI-gated. A regression whereallocandnum_traits::Floatimports were missing from the no-std path is fixed. A dedicated CI job now builds and tests the crate under--no-default-featuresto prevent future regressions.std/no_stdabstraction layer in policy.table_cacheandfloat_seednow live insrc/policy/shims that select thestd-backed (thread-local, memoised) orno_std(recompute) implementation at compile time, instead of havingstd/no_stdconditional logic scattered across algorithm bodies.src/int/integer layer layout. Thewide_int/sub-crate is absorbed; integer code is reorganised undersrc/int/types/,src/int/policy/,src/int/algos/, andsrc/int/limbs/mirroring the decimal six-bucket layout. The formerFixedInt/WideStorage/WideInttraits are unified into the singleBigInttrait, rooted insrc/int/types/traits/.(N, SCALE)policy-matcher dispatch layer. Every transcendental family (sqrt/cbrt/exp/ln/pow/trig) routes its typedDxx<S>method shell through a per-family policy trait whose body is aconst fn select::<N, SCALE>()returning anAlgorithmenum, dispatched via an inlineconst { select::<N, SCALE>() }match. BecauseNandSCALEareconstat every monomorphisation, the selector const-folds to its single live arm — each concrete type compiles to a direct call to one kernel with zero runtime dispatch cost. The integer layer carries its own parallel matcher families (mul,div_rem) undersrc/int/policy/. This replaces the previous scattered per-methodcfg/ width-branch dispatch.- Stable cross-width-and-scale conversion:
convert_from/convert_from_with. A new method on every width converts from any other width and scale in one step (e.g.D76::<40>::convert_from(some_d38_s12)), rescaling and resizing together, with a_with(mode)sibling for the rounding choice. Complements the existing same-scaleFrom/TryFromwidening / narrowing and the cross-scale_ofoperator surface. - Precision harness unified.
tests/ulp_strict_golden.rsand the comparative precision suite share a singlePrecisionSubjectharness with committable result TSV files underresults/precision/. TheprecisionCI workflow is extended with alib-cmp-precisionself-refresh that regenerates the TSVs after a clean matrix pass. Precision tables inREADME.mdanddocs/benchmarks.mdare generated from those TSVs viarender_docs.pyto prevent prose drift. - Pre-merge CI gates.
.github/workflows/ci.ymlruns on every pull request and every push tomain/release/*, enforcing: fullcargo testunder all feature combinations (including default features),cargo doc --no-deps -D warnings(catches broken intra-doc links before merge), and an informationalcargo clippy --libstep. A separate commit-hygiene gate rejects attribution trailers in commit messages. bench-fullself-refresh. After each per-width matrix sweep, thebench-fullworkflow commits a refresheddocs/benchmarks.mdso bench data and prose stay in lockstep without a manual step.- Architecture documentation.
docs/ARCHITECTURE.mdships a Mermaid layer diagram and sequence diagram showing the integer layer's own dispatch / algorithm tiers alongside the decimal layer.src/int/folders mirrorsrc/under the diagram.mkdocs.ymlenables Mermaid rendering. RELEASING.mdand release PR checklist template. The release system of record (versioning policy, branch workflow, docs + benchmark refresh, publish steps) is codified inRELEASING.mdand a PR checklist template. Performance benchmarking is advisory (not a required gate).- Checked transcendental siblings. Every strict transcendental
(
exp/ln/log/log2/log10/sqrt/cbrt, the trig / inverse-trig / hyperbolic families,powf,hypot, …) gains a non-panickingchecked_*pair returningOption<Self>:checked_<fn>_strict(self, …)and its_with(mode)sibling.Noneis returned exactly where the default strict form would panic — a domain error or an out-of-range correctly-rounded result; an in-rangechecked_*result is bit-identical to the default form's. - Crate-default rounding-mode features. Five mutually-exclusive
rounding-*Cargo features —rounding-half-away-from-zero,rounding-half-toward-zero,rounding-trunc,rounding-floor,rounding-ceiling— select theRoundingModeused by the no-argrescale/ strict /_offamily. With none enabled the default is half-to-even (banker's rounding); every*_with(mode)method keeps an explicit-mode form regardless of the feature.
[0.4.4] — 2026-05-21¶
A correctness release. Every *_strict transcendental is now provably
correctly rounded — within 0.5 ULP, i.e. 0 LSB of error at the storage
scale — under all six rounding modes, at all thirteen widths,
across the full 22-function surface. 0.4.3 secured the primary
functions under nearest rounding; 0.4.4 extends the guarantee to the
directed rounding modes and the derived functions, with no exceptions
and no ignored cells.
Fixed¶
- Directed rounding (
Trunc/Floor/Ceiling/HalfTowardZero/HalfAwayFromZero) for the wide-tier transcendentals: the working-scale approximation could previously round one LSB the wrong way near a storage grid line. Resolved with residual-sign Ziv escalation; the nearest-mode fast path is unchanged. - Derived transcendentals
log/log2/log10/exp2/sinh/cosh/tanh/asinh/acosh/atanh/powfare now correctly rounded under every mode (they previously applied their final rounding without the directed-rounding escalation, and several carried cancellation or overflow-edge error at extreme inputs). acoshnear 1 andatanhnear ±1: removed catastrophic cancellation by reformulating throughlog1p, computing1∓xandv²−1as exact working-scale gaps.sinh/cosh/tanhat large arguments: corrected a sign-direction error that turned the relative error ofe^−|x|into a large absolute error in its reciprocal; the dominant term is now always evaluated at|x|. Wide tiers near the storage-overflow edge now compute in a wider working integer.log_b(b^k)and exact-powerpowf(e.g.powf(4, 0.5)) return the exact result under directed rounding.tanhtiny arguments: directed rounding of the compressing linear band.D38log2/log10at maximum scale: removed ani128overflow in the exact-power detection.
Changed¶
- The correct-rounding guarantee — previously documented for the primary functions under nearest rounding — now holds for the entire transcendental surface under all six rounding modes and all widths.
Internal¶
- Added correctly-rounded integer
log1p/expm1kernels (internal; a public API is planned). - Expanded the strict-golden suite to the full 22-function × 13-width ×
6-mode matrix, asserting
delta == 0against an external mpmath oracle (286 cells, none ignored).
[0.4.3] — 2026-05-20¶
A correctness-and-perf release. Closed every precision hole found by
a new external-oracle test suite, fixed two latent wide-tier overflow
bugs, and improved wide-tier mul while adding a cross-scale API.
Added¶
- Cross-scale
_ofoperator surface on all 13 widths (stable):mul_of/add_of/sub_of/div_of/rem_of/max_of/min_of/clamp_of(+_with(mode)siblings) and comparatorscmp_of/eq_of/lt_of/ … accepting any-narrower-width any-SCALE operands via the new publicWidthLE<Target>trait, plus cross-widthPartialEq/PartialOrdat equal SCALE. A nightly-gatedcross-scale-opsfeature adds auto-inferredcross::{mul,add,sub,div,rem,…}free functions. - mpmath external-oracle precision suite —
tests/ulp_strict_golden.rs(golden tables generated atmp.dps ≥ 2·SCALE + 64),tests/ulp_proptest.rs(14 hard-input category strategies), and a 7361-case hard-input corpus covering half-ULP ties, catastrophic cancellation, range-reduction breakpoints, near-pole conditioning, and inverse-identity round-trips. Wired into theprecisionCI gate. - powf integer-exponent fast path (
|n| ≤ 64) viapowi, bit-exact and ~107× at D38<19> / ~27× at D307<150> for integer exponents. - Tang
lndeep-band lookup slots extending the ladder to D230<115>, D307<290>, D616<590>, D924<900>, D1232<1200>. - Const
POW10_TABLEfor the D38–D616 tiers (get_uncheckedafter a bounds guard); ~22% on D76<35>ln, ~19% onsin. - Newton–Raphson reciprocal divide for the
÷ 10^SCALEstep, dispatched at the bench-validated cells (Int2048 ≥ s200, Int3072 ≥ s200, Int4096 ≥ s400). Cite a textbook reciprocal iteration; MG magic-multiply remains canonical elsewhere. - AGM precision lift —
ln_strict_agm/exp_strict_agmnow hold 0.5 ULP at every wide tier (per-call working-scale guard sized to absorb thesqrt/2^kamplification). The dispatcher keepsln_strict/exp_stricton the artanh / Tang path; lifted AGM stays the alternate path (it loses to artanh / Tang at every shipped tier × SCALE measured). bench-allworkflow — one-click orchestrator dispatching the full release sweep (per-widthfull_matrix+lib_cmp+ cross-versionbench-history).
Fixed¶
- D153 / D1232
expprecision at large|v|— the narrow-GUARD Tangexpkernel reassembled2^k · exp(s)at a fixed working scale, amplifying thek · ln 2reduction residual by up to2^k.tang_exp_fixednow lifts the working scale by⌈|k|·log₁₀2⌉ + guardand narrows back, restoring 0.5 ULP. - D1232 transcendental-constant truncation — the u128 magnitude
buffer in
mg_divide::div_wide_pow10{,_chain}_withwas sized for Int8192 (64 u128 limbs) and silently truncated the Int16384 work integer, corruptingpi/ln 2and every constant at D1232 working scales ≳ 620. Buffer now sized to the work integer'sWideInt::U128_LIMBS. - D115 high-scale transcendental overflow — D115's transcendental
work integer was
Int1024, too small for thet·t ≈ 1040-bitintermediate inln 2evaluation at SCALE 114;exp_strict/ln_strictpanicked. Promoted D115's work integer toInt2048. tannear-pole precision (d153 / d307) —tan = sin/cosamplified the working-scale rounding by1/cos ≈ |tan|near odd multiples of π/2. The lookup sincos kernels now lift the working scale by⌈log₁₀|tan|⌉ + guardproportional to pole proximity.
Changed¶
- Width-adaptive
mg_dividebuffer restores and improves widemul(D76 mul ~52 ns, D307 mul ~122 ns at the history SCALEs — faster than v0.4.2), by sizing the÷ 10^SCALEstack buffer to the work integer instead of a flat 128-limb array. - Parity tolerances between Tang lookup and
wide_kernelpaths tightened to ≤ 1 storage LSB at every audited cell. ALGORITHMS.mddocuments the Tang table-driven kernels and an "evaluated and not used" section (Comba, CORDIC, Johansson denominator-collection, libmpfr port, Newton reciprocal trade-offs);ROADMAP.mdandCONTRIBUTORS.mdrefreshed;docs/benchmarks.mdand all figures regenerated from the v0.4.3 sweep.
[0.4.2] — 2026-05-19¶
A perf release. Every wide tier shipped a bespoke narrow-GUARD Tang
lookup slot for ln at its popular mid-storage SCALE, plus shared
kernel and infrastructure wins. Headline measured speedups at the
Tang ln slot (relative to v0.4.0 at the same cell): D115<57>
12.7×, D153<76> 19.4×, D307<150> 27×, D462<230> 27.6×, D616<308>
26.1×, D924<461> 30.8×, D1232<616> 33.8×.
Added¶
- Tang
lnnarrow-GUARD lookup slots at every wide tier: D57<20>, D115<57>, D153<76>, D307<150>, D462<230>, D616<308>, D924<460>, D1232<615>. Each is M=128 with GUARD_NARROW=8 (10 for atan/inverse). Cite Tang 1989 / 1990 (ACM TOMS 16(4)). - Tang
expsurface lookup at D57<18..=22>, D115<57>, and D153<70..=82>. At D307 and wider the surface-Tangexploses to the canonical adaptive Smith r/2^nwide_kernel::exp_fixed(the Tang post-reduction Taylor needs more wide mults than the Smith squaring tail at Int3072+); the Tangexplookup modules ship at D307 / D462 / D616 / D1232 but are not wired inpolicy::exp. Thetang_exp_fixedhelper is consumed by the hyperbolic kernels (sinh / cosh / tanh) at every tier where the trig lookup exists, in combination with the reciprocal-divide identity for an additional 1.2–1.31×. - Narrow-GUARD trig family (sin / cos / tan / atan / sinh / cosh / tanh) per tier; 1.09–1.83× speedups.
- MG-chain bit-exact half-to-even for w > 38 in
algos::mg_divide::div_wide_pow10_chain_with; promoted to production inmacros::wide_transcendentalround_to_storage_with. Audited at 642K cross-witness inputs across all 6RoundingModes. Cite Möller–Granlund 2011 IEEE TC 60(2). limbs_mul_u64_into<L, LP1>primitive inwide_int/mod.rs— Knuth Algorithm M specialised to n = 1. Wired into the wide-tiermul_uchoke-point.- f64-bridge Newton seed for
sqrt+cbrtat the wide-tier macro core. Cite Brent–Zimmermann 2010 §5.4. benches/agm_vs_taylor_d{230,616,924,1232}.rs— reproducible AGM-vs-artanh probe at deep SCALE.
Changed¶
exp(-v)→1/exp(v)reciprocal-divide identity in sinh / cosh / tanh cuts the secondexp_fixedcall; ~2–2.4× at wide tier, ~1.5× at D38.Fixed::sqrtPythagorean jointsin_cosrefactored to shared-reduction-only at D38 —sqrtat the 256-bit Fixed costs more than a second Taylor at this width; reverted to shared-reduction. (Distinct from the wide-tiersqrt_fixedwhere the Pythagorean still pays.)mul_ucentral choke-point routes through the newlimbs_mul_u64_intoprimitive whenn ≤ u64::MAX; D76<35>ln−11 % (the choke-point lives inscale_by_kwhich is called multiple times per call).- D18 (i64)
mul/divfor SCALE ≥ 10 now uses a two-limb base-2^64 schoolbook divide (Knuth Algorithm D) instead of the__divti3soft-call; D18<18> mul −60 % (9.2 → 3.5 ns), div −47 % (9.8 → 5.1 ns).
Fixed¶
- Several
/ lit(2)divisions in inverse-trig and hyperbolic paths replaced with>> 1;q % lit(2)replaced withq.bit(0);v / d + v % dreplaced withv.div_rem(d). ~30 sites across the wide-transcendental macro and the policy / trig dispatch.
Performance — findings¶
- Tang
lnladder peaks at D1232<615>: 33.8× over v0.4.0 at the slot's exact SCALE. The ladder is monotone in tier except D924<461> where the larger table OOSes L2 on most x86 (still 30.8× over v0.4.0). - Smith r/2^n joint sin/cos Taylor LOSES at narrow / medium
tier. Three-way confirmed across D38, D57, D76. Only
theoretical wins start at D307+ (where the un-reduced Taylor
balloons to 50+ terms). We retain the adaptive in-kernel Smith
path in
exp_fixed. - Comba diagonal-layout schoolbook DIES at every n. Production
row-major LLVM-unrolled
limbs_mul_u64_fixedwins 14–92 % at n = 2..16. - Karatsuba / Toom–Cook crossovers don't fire at any shipped
width per
ROADMAP.md— schoolbook + LLVM unroll plus the recursive split's heap allocs push the crossover past D1232. - Brent's AGM
lncrossover empirically located at D1232 SCALE 1000 — 3× past the textbook ~300-digit prediction. Our chain-MG + narrow-GUARD artanh kernel is well-tuned enough to delay the crossover. AGM not wired (precision lift required first; reserved for 0.5 cycle). Cite Brent 1976 JACM 23(2), Salamin 1976 Math Comp 30. - D9 (i32) genuinely exhausted — LLVM inlines Granlund–Möller
for
/ 10^9already.
[0.4.1] — 2026-05-19¶
Removed¶
DecimalConststype alias — the deprecated alias for theDecimalConstantstrait (deprecated since 0.3.3 with note "removed in 0.4") is gone. Callers shoulduse decimal_scaled::DecimalConstants;instead. The trait surface, per-width impls, and allpi()/tau()/e()inherent methods are unchanged.
[0.4.0] — 2026-05-19¶
The 0.4 release lands four roughly independent strands in one cycle:
a mechanical breaking rename + scale-cap so every width name equals
its storage's maximum decimal-digit count and at least one integer
digit is always representable; a wide-tier transcendental rewrite
that lifts a unified algorithm library into src/algos/ and routes
every typed shell through per-family policy traits in src/policy/;
a src/ reorganisation into six narrative buckets (types/,
identity/, algos/, policy/, wide_int/, macros/,
support/); and a supply-chain / governance pass (OpenSSF Best
Practices "Passing", OpenSSF Scorecard, cargo-audit + Dependabot
in CI, SECURITY.md, REUSE-compliant LICENSES/). Performance
work landed alongside: D38 transcendentals "borrow" the wider D57
work integer to shrink the strict path 5–43×, bespoke D57 lookup
kernels carry the SCALE 20 / 44–56 cells, and the wide-tier
ln / exp / sin numbers improve 34–244× versus the 0.2.5
baseline (see docs/benchmarks.md §3 and the History section).
Breaking¶
- Six widths renamed to match the true digit ceiling of their underlying storage:
| Old | New | Storage |
|---|---|---|
D56 |
D57 |
Int192 |
D114 |
D115 |
Int384 |
D461 |
D462 |
Int1536 |
D615 |
D616 |
Int2048 |
D923 |
D924 |
Int3072 |
D1231 |
D1232 |
Int4096 |
The seven widths whose name already matched (D9, D18, D38,
D76, D153, D230, D307) keep their names.
MAX_SCALEcapped atname - 1on every width. ADxxx<SCALE>withSCALE == namerepresents only|x| < magic_constant(no integer digit guaranteed). The cap restores the invariant that every representable value has at least one integer digit:
| Type | Old MAX_SCALE |
New MAX_SCALE |
|---|---|---|
D9 |
9 | 8 |
D18 |
18 | 17 |
D38 |
38 | 37 |
D57 (was D56) |
57 | 56 |
D76 |
76 | 75 |
D115 (was D114) |
115 | 114 |
D153 |
153 | 152 |
D230 |
230 | 229 |
D307 |
307 | 306 |
D462 (was D461) |
462 | 461 |
D616 (was D615) |
616 | 615 |
D924 (was D923) |
924 | 923 |
D1232 (was D1231) |
1232 | 1231 |
-
Maximum-SCALE alias types removed. Each width used to expose a scale alias at the old
MAX_SCALE; those aliases are now gone:D9s9,D18s18,D38s38,D57s57(wasD56s57),D76s76,D115s115(wasD114s115),D153s153,D230s230,D307s307,D462s462(wasD461s462),D616s616(wasD615s616),D924s924(wasD923s924),D1232s1232(wasD1231s1232). The new ceiling is the one-less alias (D38s37,D76s75, ...). -
Per-scale construction macros for the old MAX_SCALE removed.
d9s9!,d18s18!,d38s38!,d76s76!,d153s153!,d307s307!no longer exist. The proc-macro entry points (d38!,d76!, ...) now rejectscale == namewith a clearer "scale N exceeds max for Dxxx (max = N-1)" error. -
Cargo feature flags renamed for the six renamed widths:
| Old | New |
|---|---|
d56 |
d57 |
d114 |
d115 |
d461 |
d462 |
d615 |
d616 |
d923 |
d924 |
d1231 |
d1232 |
The umbrella features (wide, x-wide, xx-wide) keep their names
and have been updated to reference the new per-width names.
Migration¶
For most users the upgrade is a mechanical search-and-replace:
- Rename type imports:
D56→D57,D114→D115,D461→D462,D615→D616,D923→D924,D1231→D1232. - Rename feature-flag mentions in your
Cargo.toml:d56→d57etc. - If you used a maximum-SCALE alias or wrote
Dxxx<MAX_SCALE>literally, drop the SCALE by one:D38<38>→D38<37>,D76<76>→D76<75>,D56<57>→D57<56>, etc. - The renamed per-scale macros are gone:
d38!(_, scale 38)→ write the value at SCALE 37 instead, or useD38<37>::from_bits(...)directly.
Fixed¶
- Wide-tier
FromStrceiling lifted. Decimal strings can now be parsed at every legalSCALEon every wide-tier width — previously a u128 intermediate inside the parser silently capped the parseable scale at 38, so e.g."0.5".parse::<D307<150>>()would fail with a precision error despite the type being well within storage range. The parser now widens through the storage integer directly, matching the round-trip contracts.parse::<T>() == Ok(T::from_str(&s).unwrap())at everySCALE ≤ MAX_SCALE.
Added¶
SECURITY.md— vulnerability reporting policy, supported versions table, scope, and disclosure timeline. Required for OpenSSF Best Practices conformance.CONTRIBUTORS.md— contributor acknowledgement file at the repository root.LICENSES/directory — REUSE-compliant layout. The top-levelLICENSE-MIT/LICENSE-APACHEtext files were moved intoLICENSES/MIT.mdandLICENSES/Apache-2.0.md, and a newLICENSES/THIRD-PARTY.mdenumerates third-party origins for the vendored algorithms.- OpenSSF Best Practices badge — project 12895 reached the "Passing" tier. README badge ribbon extended with the badge.
- OpenSSF Scorecard workflow (
.github/workflows/scorecard.yml) — weekly Scorecard run, results published as a repository badge. cargo-auditworkflow (.github/workflows/cargo-audit.yml) — RustSec advisory check on every push tomain, badge surfaced in the README. GeneratesCargo.lockin-CI since the repo gitignores it.- Dependabot configuration (
.github/dependabot.yml) — daily updates for Cargo, GitHub Actions, and thebench-history/sub-crate. - CodSpeed continuous performance tracking
(
.github/workflows/codspeed.yml) — wide-tier benches + a ULP precision gate run on every PR. bench-fullworkflow (.github/workflows/bench-full.yml) — matrix fanout across all 13 widths; each shard runs in its own runner so a multi-hour sweep no longer loses every completed width on a single VM eviction.bench-historyworkflow (.github/workflows/bench-history.yml) — cross-version harness inbench-history/thatcargo adds each shipped release into a stub crate and benches them under matched Criterion settings, driving the newdocs/benchmarks.mdHistory section and the per-widthdocs/figures/history/*.pngcharts.bench-trialworkflow (.github/workflows/bench-trial.yml) — focused single-cell trial bench for quick perf experiments without paying the full-matrix runtime.precisionworkflow (.github/workflows/precision.yml) — ULP precision gate against the in-treeexamples/ulp_report.rsbaseline; surfaced as a separate README badge.DynDecimalobject-safe façade — runtime-polymorphic trait that lets callers hold aBox<dyn DynDecimal>across different widths / scales when the concrete type is only known at runtime. Re-exported from the crate root.- Unified
D<S, SCALE>struct — every shipped width (D9 / D18 / D38 / D57 / D76 / D115 / D153 / D230 / D307 / D462 / D616 / D924 / D1232) is now a type alias over a single genericD<S, SCALE>carrier rather than a hand-rolled struct per width. The alias names and their public API surface are unchanged. - Per-family policy traits in
src/policy/—SqrtPolicy/CbrtPolicy/LnPolicy/ExpPolicy/PowPolicy/TrigPolicy. The typed shell for each width picks its kernel byimpling the matching policy trait against routines fromsrc/algos/, instead of macro-emitting one open-coded copy of the kernel per width. - Algorithm library in
src/algos/—sqrt/,cbrt/,ln/,exp/,pow/,trig/each ship the kernel variants the policies route to: awiden_to_d38path for the narrow tier, aborrow_d57path that promotes D38 to D57 for tighter strict kernels, afixed_d38path for the 256-bit handrolledFixed, awide_kernelfor D76+, and bespokelookup_d57_*tables for the SCALE 20 sqrt and SCALE 18–22 / 44–56 sincos / atan cells. borrow_d57bridge — D38 transcendentals (ln,exp,pow,sin,cos,tan,atan) gain a widen-to-D57 path that runs the strict kernel at the wider work integer and narrows back. Closes the SCALE 23+ accuracy gap and drops cost multiple × on the narrow tier (see Performance).docs/comparisons.md— moved-out home for the README's binary-vs-decimal-fraction explainer and the long-form comparison with other Rust decimal crates. Wired into the docs site nav alongsidewidths.md,getting-started.md,strict-mode.md, andfeatures.md.
Changed¶
src/reorganised into six narrative buckets. Files moved:types/— public typed shells (D9/D18/D38/D57/ …), the unifiedD<S, SCALE>carrier,traits/(theDecimal/DecimalArithmetic/DecimalConvert/DecimalTranscendental/DecimalConstants/DynDecimalsurface),arithmetic.rs,rescale.rs,num_traits.rs,overflow_variants.rs,powers{,_fast}.rs,log_exp{,_fast}.rs,trig{,_fast}.rs,consts/, andwidths.rs(the formercore_type.rs).identity/— cross-typePartialEq/PartialOrdbetween every width and the primitive integer / float surface (the former top-levelequalities.rs).algos/— kernel library described above, plusfixed_d38.rs(256-bitFixed, formerlyd_w128_kernels.rs) andmg_divide.rs(formerly top-level).policy/— per-family routing traits.wide_int/— unchanged in role; the hand-rolledInt192/Int384/ … storage family.macros/— unchanged in role; declarative + procedural macros that emit the typed shells.support/—error.rs,rounding.rs,display.rs,serde_helpers.rs,bench_alt.rs,diagnostics.rs.- Typed shells route through policy traits, not direct
algos::calls. EachDxxx::ln_strict(etc.) body is now a single<Self as LnPolicy>::ln_impl(self, …)dispatch, emitted via the existing macro surface. The layering shim that let typed methods reach straight into the kernel during the pilot is gone. - README — binary-vs-decimal-fraction explainer moved out
to
docs/comparisons.md; in-README copy slimmed to a one- paragraph pointer. Badge ribbon now carries OpenSSF Best Practices, OpenSSF Scorecard, and cargo-audit badges alongside the existing crates.io / docs.rs / MSRV / License / site / CodSpeed badges. Thedocs.ymlGHA badge is now labelled "site" to disambiguate from docs.rs. AGENTS.mdand.claude/skills/decimal-scaled/SKILL.mdrefreshed for the 0.4.0 layout: new width names, new src/ bucket map, the six-bucket layering rule (typed shells call policy, policy calls algos, algos may call wide_int but never the reverse).docs/benchmarks.md— every per-tier arithmetic table (§1) and strict-transcendental table (§3) regenerated from the v0.4.0 sweep. New History section (§7) plottingdecimal-scaledv0.2.5 → v0.4.0 deltas at D38 / D76 / D307 for the same operations. New §0.4.0 raw-data dump appended in place of the prior 0.3.2 dump. Run-conditions preamble spells out the GitHub-hosted runner caveat (1.5–2× wall- clock variance per Criterion's standing guidance).
Fixed¶
- Wide-tier
FromStrceiling lifted. Decimal strings can now be parsed at every legalSCALEon every wide-tier width — previously a u128 intermediate inside the parser silently capped the parseable scale at 38, so e.g."0.5".parse::<D307<150>>()would fail with a precision error despite the type being well within storage range. The parser now widens through the storage integer directly, matching the round-trip contracts.parse::<T>() == Ok(T::from_str(&s).unwrap())at everySCALE ≤ MAX_SCALE. D38<MAX_SCALE>.log10_strictno longer panics on inputs inside the storage range but close to the upper boundary. The intermediatemul_divstep was overflowing the narrow work integer at the boundary; the fixed path uses the wider work integer thatlog_strictalready uses.D57<MAX_SCALE>.cbrt_strictno longer overflows the work integer at the maximum scale. The cube-root reduction needed one extra guard digit at the storage ceiling.- Wide-tier
cbrt_strictno longer overflows the work integer atSCALE = MAX_SCALE. Generalises the D57 fix above to every wider tier. - Overflow panic messages across every narrowing kernel now spell out which kernel + which direction + the offending value, instead of the prior generic "rescale: scale-up overflow" formatting. Surfaces the right call site immediately in downstream stack traces.
mul_div_candidatesdivide rounding in the handrolled sanity-check path realigned with the production divide kernel. Previously the handrolled path rounded half-to-even off-by-one at the LSB on a small input class, masking real regressions in the production kernel.- Test gates promoted to module-level
#[cfg(...)]. A handful of test modules added in 0.3.x still carried runtimeif !COND { return; }early-returns; those are now module-level cfg gates so a test never silently no-ops under a feature combination that disables its precondition. - Infinite recursion in
DecimalTranscendentaltrait impl — one trait-method body called the trait method instead of the underlying inherent method, producing a runtime stack overflow when called via the trait. Fixed. Fixed::rescale_downargument order is nowdebug_assert- ed at call sites — a defensive guard added after a near-miss during the policy-trait wiring.
Removed¶
- Top-level layering shims —
src/lib.rsno longer re-exports symbols that have moved into the bucketed layout for backward source-path compatibility. Public callers using the documented crate-root re-exports (decimal_scaled::D38, etc.) are unaffected; only callers that reached into the private path layout need to update. research/references — none ever shipped in source, docs, or commit messages; the folder remains private and out of the crate tarball.
Performance¶
All numbers below come from docs/benchmarks.md (per-width
arithmetic tables in §1, strict transcendentals in §3,
cross-version History in §7). The 0.4 column reflects the
v0.4.0 GHA sweep on shared ubuntu-latest runners; cold-dev-box
re-runs typically measure 30–50 % faster on the wide tiers and
several × faster on the narrow tier's picosecond cells. Treat
the multiples as directional shape-of-change rather than as
drop-in benchmarks.
- D38 strict transcendentals "borrow" the D57 work integer.
D38
log_strict/log10_strict/exp2_strict/log_strict/powf_strictnow widen to D57 for the integer-kernel pass and narrow back. The D57 borrow path is also scale-gated for D38powf(SCALE >= 23, tightened from the originalSCALE >= 25after measurement). - Bespoke narrow-GUARD kernels on D57. Hand-rolled kernels
for the SCALE 20 sqrt (
algos::sqrt::lookup_d57_s20), the SCALE 18..=22 sincos (algos::trig::lookup_d57_s18_22_sincos), the SCALE 44..=56 sincos / atan (algos::trig::lookup_d57_s44_56_*) and the SCALE 45..=56 exp (algos::exp::lookup_d57_s45_56) carry the cells where the generic wide kernel was the wrong shape for the scale. - D38 cos closes ~25 % of the gap to D38 sin (joint sin / cos kernel re-tuned).
- D38 sqrt + div u128 fast path skips the 256-bit machinery when the inputs fit u128, recovering several ns per call on the narrow tier.
- Wide-tier transcendentals (D76+) —
pow10(w)memoised per-tier in the wide transcendental cores. The cross-version delta vs the 0.2.5 baseline on the same operations, fromdocs/benchmarks.md§3:
| op | 0.2.5 | 0.4.0 | speedup |
|---|---|---|---|
| D76<35> ln | 1.37 ms | 40.6 µs | 34× |
| D76<35> exp | 1.27 ms | 32.1 µs | 40× |
| D76<35> sin | 1.08 ms | 22.6 µs | 48× |
| D76<35> sqrt | 20.5 µs | 3.53 µs | 6× |
| D153<75> ln | 6.40 ms | 67.5 µs | 95× |
| D153<75> exp | 5.87 ms | 53.8 µs | 109× |
| D153<75> sin | 4.82 ms | 40.8 µs | 118× |
| D153<75> sqrt | 83.6 µs | 4.98 µs | 17× |
| D307<150> ln | 34.1 ms | 161.3 µs | 211× |
| D307<150> exp | 31.2 ms | 131.5 µs | 237× |
| D307<150> sin | 25.5 ms | 104.4 µs | 244× |
| D307<150> sqrt | 313 µs | 7.84 µs | 40× |
These multiples are conservative since the 0.2 column is a cold-dev-box run and the 0.4 column is shared CI; the algorithm-of-record changes (u64-native limbs, MG 2-by-1 reciprocal Knuth divide, Brent's two-stage exp argument reduction, multi-level sqrt halving in ln, [0, π/4] sin range reduction, sin_cos / sinh_cosh joint kernels, thread-local pi / ln2 / ln10 cache, pow10-cached mul / div per inner loop) carry the win.
Internal¶
- Build-time math-constant scales lowered:
SCALE_REFfor D153 / D230 / D307 dropped to 152 / 229 / 306 respectively (the others already matched the newname - 1ceiling). - Bench file names tracking width (
benches/full_matrix_d56.rs,benches/lib_cmp_d1231.rs, ...) renamed to match the new width identifiers. - New
src/macros/dyn_bridge.rscarries theDynDecimalblanket- impl bridge so every typed shell picks up the object-safe surface via one macro invocation. - Wide transcendental macro module (
src/macros/wide_transcendental.rs) consolidated to route throughsrc/policy/rather than emitting one open-coded kernel per width. - Layering rule codified: typed shells call policy, policy calls
algos, algos may call
wide_intbut never the reverse. Cargo.tomldev-dependencyfastnumbumped 0.4 → 0.7; benches follow the new API.
[0.3.3] — 2026-05-18¶
Trait surface split + benchmarks refresh, all in one cycle.
Changed¶
Decimaltrait is now a marker supertrait combining four narrower halves:DecimalArithmetic(operators, sign, pow / checked / wrapping / saturating / overflowing, reductions, plus the foundational type infoStorage,SCALE,MAX_SCALE,ZERO,ONE,MAX,MIN,multiplier()),DecimalConvert(from_bits/to_bits/scale, integer bridges, thestd-gated f64 / f32 bridge),DecimalTranscendental(the four-variant transcendental + root matrix), andDecimalConstants(pi/tau/e/ …). Decimal carries a blanket impl so every type that implements all four halves is Decimal for free; every shipped width (D9..D1232) keeps the full surface.
Callers using T: Decimal and T::method() syntax keep
compiling unchanged — method resolution finds methods via the
supertrait chain. The narrower bounds let generic code shrink
its surface when it doesn't need everything, e.g.
fn dot<T: DecimalArithmetic + Copy>(a: &[T], b: &[T]) -> T.
Breaking¶
<X as Decimal>::methodUFCS path callers need to move to the right sub-trait:<X as DecimalArithmetic>::ZERO/::MAX_SCALE/::multiplier/::sum/::is_zero/ etc., and<X as DecimalConvert>::from_bits/::to_int_with/::from_f64/ etc. Test files undertests/and the internal tests insrc/core_type.rshave been updated as part of this release.
Added¶
DecimalArithmeticandDecimalConvertas new narrower trait surfaces (see Changed). Re-exported from the crate root.
Benchmarks¶
docs/benchmarks.mdrefreshed from the 2026-05-18 sweep. Every per-tier arithmetic table (§1) and every strict- transcendental table (§3) regenerated with the new numbers, preserving narrative around them.- Caveats called out inline: narrow-tier (D9 / D18 / D38) picosecond-scale arithmetic sits near the bench machine's resolution floor and the cross-version deltas there are contention noise rather than real perf; narrow-tier transcendental values in this sweep also reflect contention drift (cold-machine micro-bench for D38<19> ln_strict measures ~54.8 µs vs the >800 µs in the full_matrix table — ~15× gap). Wide-tier numbers (D57+ arithmetic, D57+ transcendentals) are reliable and reflect real cumulative perf work from the 0.3.x cycle.
- The 326-measurement raw data dump from 0.3.2 stays appended at the end of the doc for anyone who wants every cell criterion produced.
[0.3.2] — 2026-05-18¶
API-additive surface expansion across every decimal width plus
several downstream-blocking defect fixes. No breaking changes;
existing callers compile unchanged. Big themes: the four-variant
_strict_with / _approx / _approx_with matrix on every
transcendental, mode-aware constants, full serde parity, the
construction-macro surface extended to every shipped tier, a docs.rs
build fix, and primitive ergonomics on the wide-int and
fixed-decimal types.
Headline perf number for the new _approx family on the D38<19> ln
sanity bench: 1.17× faster at guard 25, 1.45× at 20, 1.80× at 15,
2.62× at 10, 3.70× at guard 6 — all vs strict's 54.8 µs. Strict
itself is at parity with 0.3.1 (52.3 µs prior → 54.8 µs now, within
bench noise). Full per-tier numbers in 0.3.3.
Added¶
- Four-variant matrix on every transcendental. Each function
ships
<fn>_strict,<fn>_strict_with(mode),<fn>_approx(working_digits), and<fn>_approx_with(working_digits, mode). The_strictbody keeps a const-foldedSTRICT_GUARDso LLVM specialises one optimal kernel perSCALE;_approxtakes the guard width at runtime for callers who want to trade ULP precision for latency. Whenworking_digits == STRICT_GUARDthe_approx_withbody redirects to_strict_withso the fast path is never displaced. Covers ln / log / log2 / log10 / exp / exp2 / powf on D38 and the wide tiers, and sin / cos / tan / atan / asin / acos / atan2 / sinh / cosh / tanh / asinh / acosh / atanh / to_degrees / to_radians for the trig family. sqrt_strict_with(mode)/cbrt_strict_with(mode)/hypot_strict_with(mode)on D38 and the wide tiers. Roots have no working-digits parameter (the exact-integer-root path is precision-independent), so only the_strict/_strict_withpair exists.FloorandCeilingdispatch by sign forcbrt.DecimalConsts::*_with(mode)siblings.pi_with/tau_with/half_pi_with/quarter_pi_with/golden_with/e_withrescale the embedded reference under a caller-suppliedRoundingMode.pi_with(Floor)gives the largest representable value ≤ true π — useful for CAD tessellation that must not over-count.from_num/to_numparity across every width. The saturatingNumCast-style bridge previously existed only on D38; extracted intodecl_decimal_num_traits_basics!so D9 / D18 / D38 / D57 / D76 / D115 / D153 / D230 / D307 / D462 / D616 / D924 / D1232 all get the same surface.- Serde for every wide tier. Added
decl_wide_serde!invocations for D57 / D115 / D230 / D462 / D616 / D924 / D1232 — previously only D76 / D153 / D307 had Serialize / Deserialize. proc-macro-crateresolution in the construction macros (d9!,d18!,d38!,d76!,d153!,d307!). Lets consumer crates aliasdecimal-scaledfreely instead of being forced to use the literaldecimal_scaledimport name. Falls back to::decimal_scaledwhen the lookup fails — same behaviour as the original hard-coded path.- Construction macros for every wide tier. Added
d57!,d115!,d230!,d462!,d616!,d924!,d1232!— every shipped Decimal width now has a compile-time literal constructor over the existingexpand_for(Width, …)machinery, gated behind the matching feature. EPSILON/MIN_POSITIVEfor every decimal width. Macro- emitted out ofdecl_decimal_basics!so D9 / D18 / D38 / D57 / D76 / D115 / D153 / D230 / D307 / D462 / D616 / D924 / D1232 all share the same surface. Closes the "literal1doesn't coerce to a wide-int storage type" trap that forced downstream callers to writefrom_bits(<Int192>::from_u128(1))longhand.- Wide-int
From<primitive>impls. Every signed wide int ($S) getsFrom<u8/u16/u32/u64/u128/i8/i16/i32/i64/i128>; every unsigned ($U) getsFrom<u8/u16/u32/u64/u128>plus afrom_u128const-fn constructor. - Wide-int
From<float>impls.From<f32>andFrom<f64>for both$Uand$S; nightly-onlyFrom<f16>/From<f128>gated behind the existingexperimental-floatsfeature. Same saturate-on-overflow / NaN-maps-to-zero contract the decimal-tier float bridge uses. - Wider scale-alias coverage. Backfilled D76 / D153 / D307
from the previous 5-8 aliases each to ~25 per tier covering the
standard decimal-precision breakpoints (1, 2, 3, 4, 6, 9, 12,
15, 18, 20, 24, 28, 32, 35, 38, 50, plus tier-specific midpoints
and the storage-max scale). Added
Dnns1(one fractional digit) to every wide tier so all 13 widths have a consistent scale-1 alias. AddedD57s20for the 20-digit CAD-numeric precision moocad's Stage 5 needed.
Fixed¶
- docs.rs build for 0.3.1 failed with
E0425: cannot find type D924.src/core_type.rs'sD616::widenreturnedD924<SCALE>unconditionally, butD924is gated behindxx-wide— which the docs.rs feature set didn't enable. SplitD616's impl block in two sowidenonly exists when D924 does, and addedxx-wideto the docs.rs[package.metadata.docs.rs]feature list for a complete rendered surface.
Documentation¶
- f64-boundary warning on
DecimalConsts:to_f64()is itself correctly rounded, butpi() → to_f64()atSCALE < 15lands ~466 ULPs fromstd::f64::consts::PIbecause the decimal value is intrinsically coarser than the f64 grid. Downstream code that uses the f64 to count, bucket, or seed an iteration loop should source mathematical constants fromstd::f64::constsdirectly at the boundary, or pickSCALE ≥ 15. - Module-header rewrite on
log_exp_strict,trig_strict,powers_strict, and the wide-tierwide_transcendentalmacro describing the four-variant matrix and when each form is appropriate.
Benches¶
- Split the monolithic
full_matrixbench into per-width binaries (full_matrix_d{9,18,38,56,76,114,153,230,307,461,615,923,1231}) sharing afull_matrix_common.rsmacros module. A multi-hour sweep used to lose all completed widths on a power-loss / lid close; per-width binaries let criterion flush each width before starting the next. scripts/bench_pinned.ps1andscripts/bench_sweep.ps1: 2-lane parallel runner that pins each lane to a physical core (both SMT siblings) at High priority. Skips any bench whose log already shows completion, so a partial sweep resumes from where it left off in one command.
[0.3.1]¶
Release-process patch. Library code, public API, on-wire format, and bench numbers are byte-identical to 0.3.0.
Fixed¶
- GitHub Pages docs site (
mootable.github.io/decimal-scaled) failed to refresh on the v0.3.0 tag push. The Pages environment protection rule only allows deploys frommain; the concurrent main-push run that was allowed to deploy got cancelled by the tag-push run that arrived right after. Tag-triggered deploys blocked. 0.3.1 ships as amain-branch push so the docs workflow runs to completion.
Notes¶
- Future releases should land the version-bump commit, let
mainbuild + deploy the docs, then tag — not the other way round. Will codify inscripts/deploy.ps1.
[0.3.0]¶
The half-width-tier release. The decimal ladder now goes
D9 → D18 → D38 → D57 → D76 → D115 → D153 → D230 → D307 →
D462 → D616 → D924 → D1232 — every adjacent pair has a
lossless From / widen() plus a fallible TryFrom /
narrow(). New x-wide and xx-wide Cargo umbrellas gate the
wider ranges so default builds stay lean.
The strict / fast dispatcher rule changes too: strict is now
the default plain dispatch in every build, and wins on tiebreak
when both strict and fast are enabled. The only way to
land plain sin / ln / sqrt etc. on the f64 bridge is a
deliberate three-step opt-out (default-features = false + add
fast + add std + don't re-add strict). The named
*_strict and *_fast methods stay available regardless of
feature choice.
Added — new decimal tiers¶
D57(192-bit),D115(384-bit),D230(768-bit) — half-width tiers between every existing power-of-two width. Gated behindd57/d115/d230individually or by the expandedwideumbrella (nowD57–D307together).D462(1536-bit),D616(2048-bit) — new x-wide tier, gated behindx-wide(ord462/d616individually).D924(3072-bit),D1232(4096-bit) — new xx-wide tier, gated behindxx-wide(ord924/d1232individually).- The naming rule: the number on every
D{N}type is the highest safeSCALE(MAX_SCALE) the storage can hold, i.e. the number of decimal digits you can represent without overflow. SoD1232meansMAX_SCALE = 1232(∼ 1232 decimal digits of headroom), not "1231 bits". - Comprehensive scale aliases per the new tiers: ≥ 16 per tier above the narrow range, covering 0 / common midpoints / the previous tier's MAX_SCALE as a cross-tier sentinel / the new tier's MAX_SCALE.
Changed — breaking¶
D38.widen()now returnsD57<SCALE>instead ofD76<SCALE>. Symmetrically,D76.narrow()→D57,D76.widen()→D115,D153.narrow()→D115,D153.widen()→D230,D307.narrow()→D230, andD307.widen()is new (→D462, gated behindx-wide). The legacy power-of-two-next-up semantics are gone; the comprehensive ladder is the new default. Callers that need the old jump can use.into()/.try_into()to skip rungs.strict+fastnow resolves to strict. Previouslyfastwon the tiebreak. The strict path is now fast enough (ln_strictat D38<19> is ~1.5 µs) that staying on the deterministic correctly-rounded path by default is the right call across more codepaths.(no feature)builds now dispatch plain transcendentals to*_stricttoo (previously they fell through to*_fast). Same reasoning: strict-by-default unless deliberately opted out.
Added — performance¶
- Chain-of-÷10^38 rescale for wide-tier
mulatSCALE > 38: factorsn / 10^SCALEas a sequence ofn / 10^38chunks, each riding the existing base-2^128 MG 2-by-1 magic kernel. Combined-remainder bookkeeping preserves HalfToEven correctness across chunks. Measured wins: - D307<150> mul: 786 ns → 434 ns (1.8× faster)
- D462<230> mul: 1.62 µs → 866 ns (1.9×)
- D616<308> mul: 2.20 µs → 1.36 µs (1.6×)
- D924<461> mul: 3.30 µs → 2.68 µs (1.2×)
- D1232<616>: marginal (chain length eats the per-pass win; needs Barrett or wider magic tables — tracked for 0.3.x).
d57/d115/.../d1232per-tier features can be enabled individually if you don't want a full umbrella.
Added — benches + tooling¶
- Per-width
lib_cmp_d{N}benches — 13 new bench binaries (cargo bench --bench lib_cmp_d307) replace the monolithiclibrary_comparison.rs. Each tier runs in minutes instead of hours; iterating on one tier's perf doesn't need a full matrix sweep. Shared macros + helpers live inbenches/lib_cmp_common.rs. benches/quick_div.rs— focused microbench for D307/D616/D924/D1232 div + mul. Used during the wide-tier perf tuning passes.- Per-width summary chart family — one PNG per power-of-two
storage width (
docs/figures/library_comparison/summary_{N}bit.png) showing every op (add / sub / neg / mul / div / rem / sqrt / ln / exp / sin / cos / tan / atan / sinh / cosh / tanh) on a log-y axis with one bar per library. Re-rendered automatically via thescripts/refresh_bench_artifacts.shworkflow. scripts/bench_log_to_medians.py— extracts the criterion medians from any number of bench-log files intotarget/medians.tsvso chart_gen.rs picks up the latest run.- chart_gen filter tightened: only renders multi-library line charts where ≥ 2 libraries have ≥ 2 data points, so scatter-of-dots charts get dropped automatically.
- Trig family in the bench matrix —
cos/tan/atan/sinh/cosh/tanhbenched for every peer that ships them (decimal-scaled, fastnum, g_math, rust_decimal cos/tan). examples/rounding_mode_probe.rs— diagnostic that prints the candidate renderings under each rounding mode forexp(1),sin(1),ln(2),sqrt(2). Used to verify that the §5 "1 ULP" entries onfastnum/rust_decimalwere render-mode artifacts (they carry the correct value internally), not computation errors.examples/fast_vs_strict_ulp.rs— per-tier accuracy-loss table for*_fastvs*_strict. Drives the new §2.1 indocs/benchmarks.md.
Fixed¶
- D57 work-integer overflow — D57's transcendental work
integer was Int512, which couldn't carry the squared
intermediate at
SCALE + GUARD = 86working scale. Bumped to Int1024. Caught by the in-flight bench sweep. feature = "xx-wide"-only builds — several macro arms insrc/macros/full.rsandsrc/mg_divide.rsgated only onwide/x-wideand missedxx-wide/d924/d1232, so anxx-wide-only build failed to compile. Gates extended.
Docs¶
- §5 in
docs/benchmarks.mdrewritten as "Where each crate fits" — feature-matrix-first framing, per-storage-width summary charts, an explicit note distinguishing render-mode artifacts (fastnum / rust_decimal / decimal-rs) from real precision losses (dashu-float 4 ULP exp(1), g_math 6–46 ULP). Bench-doc tone shifted from "competition" to "here's where each crate fits, here's where ours fits". docs/widths.md,docs/getting-started.md,docs/strict-mode.md,docs/features.mdall updated to enumerate the thirteen-tier ladder and describe the new strict-by-default dispatcher.- README — "Two headline guarantees" lede now promotes both ≤ 0.5 ULP correctness AND caller-chosen rounding mode at every lossy operation; tier table extended to all 13 widths.
ALGORITHMS.md— wide-int section enumerates every shipped storage type; atan halvings table extended to cover the new wide tiers; tier-listing references updated from the old four-tier list.ROADMAP.md— explicit versioning intent table (0.3.0 done / 0.4.0 signed SCALE + RNG / 1.0.0 gated on competitive wide-tier mul/div or per-row documented gap); MG magic-multiply extension + Barrett path queued for 0.3.x; out-of-tree ecosystem crates (decimal-scaled-expr / -math / -finance) with the expression-engine dual-track + whole-tree serialisation design captured.
[0.2.5]¶
Docs + benchmark accuracy patch. Library code, public API, and on-wire format are byte-identical to 0.2.4.
Fixed¶
docs/benchmarks.md- every numeric cell in the arithmetic, fast-transcendental, strict-transcendental, and wide-integer-backend tables was re-measured on a single machine in one criterion run with the default 3 s warm-up, 50-sample (D38-and-narrower) or 20-sample (wide tier) windows. The previous numbers were collected with a much shorter warm-up / fewer samples and several rows shipped unsubstituted__LOSSY_*__/__STRICT_*__template placeholders.benches/decimal_backends.rs- theD128_lossyandD256_lossyrows called the plain*dispatcher methods, which with the defaultstrictCargo feature flip to the*_strictinteger kernel. The rows therefore measured the strict path twice instead of contrasting fast vs strict. They now call*_fastexplicitly, so the fast / strict distinction shown in the docs is honest.
Changed¶
docs/benchmarks.md§2 "Fast transcendentals" - table reshaped from the unsubstituted "D9 / D18 / D38 fast" placeholders to the actually-benched "D38*_fast/ D76*_fast/rust_decimal" comparison, with a prose note that D9 / D18*_fastshare the D38 f64-bridge kernel viato_f64/from_f64and incur only a sub-ns round-trip on top of the listed D38 numbers.docs/benchmarks.mdmethodology section - warm-up / sample-size text updated to match the bench harness's actual configuration (3 s warm-up, auto-tuned measurement window, 50 or 20 samples depending on tier).docs/benchmarks.md§3 strict transcendental tables - collapsed from one column per (width, scale) to one column per width, each cell showing only the s = mid measurement (the honest series-cost scale - s = 0 hits fast-path early returns and s = max sometimes shortens via Cody-Waite range reduction, so neither is a fair comparator). The chosen mid is listed in the column header (e.g.D76 (s=35)).docs/benchmarks.mdTime units table - added picosecond row and reframed the third column as "Relative to a second" instead of "Relative tons" for consistency across the table.docs/benchmarks.mddata-cell rendering - scientific notation in data cells (e.g.1.46×10⁻³ µs) replaced with plain decimals (0.00146 µs). Scientific notation is now reserved for values smaller than 10⁻⁵ of the row's unit (none in the current tables). Time units table is unchanged (still uses10⁻³ s,10⁻⁶ s, etc. for second relationships).
Added¶
benches/library_comparison.rs- new bench that pitsdecimal-scaledagainst every viable peer on crates.io (fastnum,bigdecimal,dashu-float,decimal-rs,rust_decimal,fixed::I*F*,g_math) across all six decimal-scaled width tiers (32 / 64 / 128 / 256 / 512 / 1024 bit) at three scales per tier (s=0, s=mid, s=max).examples/ulp_report.rs- one-shot accuracy report measuring ULP error for each library'sln(2)/exp(1)/sin(1)/sqrt(2)against aD76<19>integer-only*_strictbaseline. Confirmsdecimal-scaledis 0 ULP on every transcendental tested and showsg_math's "0 ULP transcendentals" marketing claim is 6–46 ULP off at the matched precision.examples/chart_gen.rs- pure-Rust (plotters) chart generator that readstarget/medians.tsvand emits one layered line chart per (op × width) todocs/figures/library_comparison/. 60 PNGs total; the meaningful-variation subset is embedded indocs/benchmarks.md§5.docs/figures/library_comparison/*.png- 52 generated charts (every op × width combination that has measurements).docs/benchmarks.md§5 Library comparison - new chapter with one speed table per width tier (s=mid representative scale, library-by-library), an accuracy table at the 128-bit tier (ULP errors for every supported transcendental across every library), embedded charts for the meaningful-variation ops, and a "reading the comparison" buyers-guide paragraph that maps "what do you need" → "which crate".- Dev-dependencies:
fastnum,bigdecimal,dashu-float,decimal-rs,scientific,plotters- all bench/example-only, none compiled into a normal build. ROADMAP.md(repo root) - tracked list of throughput gaps surfaced by the §5 library comparison and the planned fix per item (Burnikel-Ziegler divide, Karatsuba/Toom-3 mul,*_approx(working_digits)transcendental family). Cross-linked fromdocs/benchmarks.mdRoadmap section.
[0.2.4]¶
Agent-ecosystem additions. No library code changes - the crate's public API, behaviour, and on-wire format are byte-identical to 0.2.3. The bump exists so the new agent-facing assets ship in the crates.io tarball.
Added¶
AGENTS.md(top level) - tool-agnostic usage guide consumable by Cursor, Continue, Aider, Codeium and any other agent runner that crawls a repo forAGENTS.md. Covers width / scale picking, the strict-vs-fast dual API, rounding modes,DecimalConsts, rescaling, serde format, common anti-patterns, Cargo feature cheat sheet, and quick recipes..claude/skills/decimal-scaled.md- Claude Code skill (same content asAGENTS.md) withname/descriptionfrontmatter so Claude Code can auto-discover and invoke the skill when a user prompt mentions the crate.
[0.2.3]¶
Documentation patch (and matching test additions). The 0.2.2 docs
incorrectly listed golden among the constants that don't fit
D38<38>'s storage range - the code was correct (golden ≈ 1.618 is
inside the ±1.70141 storage range, so the method returns the
correctly-rounded value), but the prose and CHANGELOG copy claimed it
panicked. Fixed.
Fixed¶
- Documentation in
consts.rs(module preamble +DecimalConststrait doc) andCHANGELOG.mdfor 0.2.2 said the larger-magnitude constants that overflowD38<38>storage were "pi, tau, e, golden".golden ≈ 1.61803actually fits the ±1.70141 storage range and the method returns the correctly-rounded value; onlypi(3.14),tau(6.28), ande(2.72) overflow. Docs corrected.
Added¶
tests/consts.rsinline mod: explicit#[should_panic]tests pinningD38<38>::pi(),D38<38>::tau(), andD38<38>::e()to the storage-overflow panic. Promotes the prior single-constant spot test to cover all three.- The
fitting_constants_at_scale_38_are_correctly_roundedtest now also assertsD38<38>::golden()is correctly rounded to 0.5 ULP (= 1 LSB) of the canonical 38-digit value.
[0.2.2]¶
DecimalConsts 0.5-ULP contract is now uniform across every supported
scale - the 0.2.0 / 0.2.1 ≈ 5 ULP "exception at D38<38>" is gone.
Changed¶
DecimalConstsreference precision - every constant on D9 / D18 / D38 is now derived from the 75-digitInt256reference (the same one the wide tier already used), rescaled down half-to-even to the caller'sSCALE. The previous code used a 37-digiti128reference and rescaled upward by 10 atD38<38>, which appended a placeholder zero and left the result ≈ 5 ULP off the canonical value. Every result on every supported scale on every width is now within 0.5 ULP of the canonical decimal expansion - the precision contract holds with no documented exceptions.D38<38>storage-range overflow - atSCALE = 38the D38 storage range is approximately ±1.7, so the four larger-magnitude constants (pi ≈ 3.14,tau ≈ 6.28,e ≈ 2.72,golden ≈ 1.62) genuinely cannot be represented. The corresponding methods previously panicked with the generic rescale messageD38::rescale: scale-up overflow; they now panic with the explicitD38 constant out of storage range: <name> cannot fit i128 at SCALE = 38.D38<38>::half_pi()andD38<38>::quarter_pi()(which fit storage) are correctly rounded to 0.5 ULP - verified by a new test asserting|result − truth| ≤ 1 LSBat the 38-digit storage scale.
Fixed¶
- The "
D38<38>≈ 5 ULP exception" mentioned in 0.2.1'sDecimalConstsmodule / trait docs is removed from both the preamble and the trait blurb; the rewritten docs state the now- uniform 0.5 ULP contract. docs/strict-mode.md"Choosing the configuration" table reflowed: the default isstrict(not the f64 bridge), and thefastfeature row no longer claims it drops the*_strictsurface (corrected in 0.2.0 elsewhere; the table row was missed).
[0.2.1]¶
Documentation patch - no API changes.
Fixed¶
- docs.rs build: the rendered crate page on https://docs.rs/decimal-scaled
was only showing the default-feature surface, so the wide-tier types
(
D76/D153/D307), thedNN!proc-macros, and theserde_helpersmodule were missing. Added a[package.metadata.docs.rs]block that enablesstd,serde,strict,macros,wide, andx-widefor the docs build. The matchingdocsrscfg is also wired so future#[cfg(docsrs)]doc-cfg badges can highlight feature-gated items. constsmodule +DecimalConststrait docs: the preamble and per- method blurbs claimed every constant was rescaled from a single 37-digiti128reference. That was true for D9/D18/D38 but ignored the per-tier raw references shipped for D76 (75 digits), D153 (153 digits), and D307 (307 digits) underconsts_wide.rs. The new docs include a per-tier reference table and an explicit statement of the precision contract: within 0.5 ULP at every supported scale on every width, with the documented exception ofD38<38>(the D38 maximum, rescaled upward by 10 from the 37-digit reference - ≈ 5 ULP bound onpi/tau/e/golden).
[0.2.0]¶
The 0.2 release rounds out the family the 0.1 line scaffolded: every
width ships the full method surface, the Decimal trait carries the
width-generic API, and the strict / fast routing is symmetric and
explicit.
Added¶
- Wide tier (D76 / D153 / D307) - the 256 / 512 / 1024-bit decimal
widths are now feature-complete. Each implements every surface D38
has: arithmetic and bitwise operators, sign methods, integer
methods, overflow variants, pow + powi + the four pow overflow
variants, cross-type
PartialEqagainst every primitive integer and float, the float bridge (from_f64,from_f64_with,to_f64,to_f32), serde round-trip, and the full strict-transcendental surface - every*_strictmethod plus a mode-aware*_strict_with(mode)sibling. Two AGM alternatesln_strict_agm/exp_strict_agm(Brent–Salamin 1976, Newton-on-AGM-ln) are exposed alongside the canonical artanh / Taylor paths. - In-tree wide-integer module (
crate::wide_int) - the wide tier is now backed by a hand-rolledInt256/Int512/Int1024/Int2048/Int4096family (plus unsigned siblings) emitted by a macro. No external big-integer dependency. Includes Karatsuba multiplication (dispatched at the 16-limb threshold), Knuth Algorithm D, and a Burnikel–Ziegler recursive divide wrapper. Decimaltrait - expanded surface - the trait now carries every uniform method every width implements: arithmetic, bitwise, and shift operators as supertrait bounds; sign (abs,signum,is_positive,is_negative); integer methods (div_euclid,rem_euclid,div_floor,div_ceil,abs_diff,midpoint,mul_add); integer-shape predicates (is_nan,is_infinite,is_finite); the full pow + checked/wrapping/saturating/overflowing pow family; the fullchecked_*/wrapping_*/saturating_*/overflowing_*ofadd/sub/mul/div/neg/rem; integer conversion (from_i32,to_int,to_int_with); the float bridge gated onstd; and default reductions (is_zero,is_one,is_normal,sum,product). PlusDebug/Display/Hashsupertraits.d9!/d18!/d76!/d153!/d307!proc-macros - matchingd38!per-width entry points, including:- per-scale wrappers (
d38s12!,d18s6!, etc.) that pre-bake the scale qualifier; - radix prefix integers (
0xFF,0o755,0b1010); - the explicit
radix Nqualifier; - fractional radix literals (
d76!(1.A3, radix 16, scale 12)); - explicit
scale Nandroundedqualifiers. *_strictand*_fastnamed methods always available - both surfaces compile in every feature configuration (subject only to dependency gates -*_fastneedsfeature = "std"). The plain*form is the only thing thestrict/fastfeatures control.widen()/narrow()hop methods - promote to the next storage tier or demote with a fallible narrowing, without the longhandFrom::from/TryFrom::try_fromsyntax.rescale_with(mode)mode-aware sibling on every width.with_scale<TARGET>()builder-style alias forrescale.*_with(mode)siblings throughout - every default-rounding operation (from_f64,to_int,rescale, etc.) now has a sibling taking an explicitRoundingMode.from_num/to_numon D38 (insrc/num_traits.rs, renamed fromfixed_compat) - saturating, never-panicking constructors and readers that thread throughnum_traits::NumCast.hypot_stricton every width - integer-only, correctly-roundedsqrt(a² + b²)via the scale-trick algorithm.
Changed¶
- Type names - public types now name themselves by safe decimal
digit capacity (
D9/D18/D38/D76/D153/D307) rather than by underlying integer bit-width. The number in the type name is also the type'sMAX_SCALE. - Strict mode is the default -
default = ["std", "serde", "strict"]. Build without default features to get the f64-bridge path. *_fast(formerly suffix-free) - the f64-bridge methods are now named*_fast(ln_fast,sin_fast, …) for symmetry with*_strict. Plain*is the feature-driven dispatcher.fastfeature contract - no longer drops the*_strictsurface; only forces plain*to resolve to*_fast.Decimaltrait supertrait bounds - extended withDefault,Debug,Display,Hash, all arithmetic /*Assignoperators, and the full bitwise / shift operator set.fixed_compat.rs→num_traits.rs- file renamed; module docs no longer reference thefixedcrate. Thefrom_num/to_nummethods themselves are unchanged.- README, docs/, and crate-level documentation rewritten to reflect the all-six-widths reality. Stale claims about D38-only-implements-trait, bnum-backed wide tier, "wide tier not yet wired", "Karatsuba is a future optimisation", and "fast drops the strict surface" are all corrected.
Removed¶
- The
bnumdependency - wide-tier storage migrated to the in-treewide_intmodule.bnumand friends remain as[dev-dependencies]for the benchmark baselines only. _lossy/_fastfloat-conversion suffixes - the float conversion methods are nowto_f64,from_f64,to_f32,from_f64_with. The historic_lossy/_fastsuffixes were redundant since there is no strict counterpart for these (they are type conversions, not transcendentals)._lossy/_intinteger-conversion suffixes dropped for the same reason -from_int/to_int/to_int_withare the only variants.- Placeholder wide-tier feature flags (
d115,d230,d462,d616,d924,d1232) - these were forward-planned widths that were never implemented. Shipping no-op feature flags would mislead users pinning to them. Will be re-added when the corresponding storage types land. - Dead code in
mg_divide- the unuseddiv_exp_fast_2wordwrapper (only the_with_remvariant has callers). - Inline test mods that ran without asserting - the runtime
if !DEFAULT_IS_HALF_TO_EVEN { return; }guard pattern was replaced with module-level#![cfg(...)]so tests never silently no-op under a non-defaultrounding-*feature.
Fixed¶
- Strict/fast routing defect - pre-0.2 the
*_strictmethods werecfg(not(feature = "fast"))and the*_fastmethods werecfg(all(feature = "std", any(not(feature = "strict"), feature = "fast"))), so in the default-strict build there was no way to call the f64-bridge methods by name, and vice versa. Both surfaces now always compile (subject tostdfor*_fast). - Module-level doc comment staleness - six modules contained D38-only narratives / "Phase N will add" / "future widths" / broken file-path references; rewritten to match the all-six-widths reality.
- Broken intra-doc links -
[Self::MIN]at module scope,[FromStr]withoutcore::str::prefix,[D38::rescale]at module scope,[num_traits::Zero]shadowed by the post-renamecrate::num_traitsmod - all fixed.cargo doc --no-deps --document-private-itemsnow reports zero warnings. - Crate-wide warning-clean build under every feature
combination -
default,--no-default-features,fast,strict,wide,x-wide, and combinations thereof. - Coverage hardening - comprehensive functional tests added for
every public surface, the wide-integer kernels,
mg_divide, the guard-digitd_w128_kernels, and every transcendental's domain panic. Tests are functional (named by behaviour, not by uncovered line) and topic-organised intests/.
Compile-time / MSRV¶
- MSRV declared as Rust 1.85 (lower bound for the 2024 edition).
Migration notes¶
- The
D128(etc.) type names are gone - they were renamed to their digit-capacity counterparts in the 0.1 line. If you pinned to a pre-rename name, update to the new spelling. - Code that called
.ln()/.sin()etc. and relied on the f64 bridge being present in the default strict build now still compiles, but the routing has been clarified - call.ln_fast()/.sin_fast()directly if you specifically want the f64 path regardless of the build's feature set. - The
_lossy/_fastsuffixes on float conversion methods (to_f64_lossy,from_f64_fast, …) have been removed across two prior renames; the methods are now justto_f64/from_f64/ etc. Update any leftover suffixed call sites. - If you depended on a placeholder wide-tier feature flag (
d115,d230,d462,d616,d924,d1232), the flag no longer exists. Usewideorx-wideto cover the implemented widths.
[0.1.1] - 2025-12¶
Bug-fix release of the initial public 0.1 line. Refer to the git
history under tag v0.1.1 for the full commit log; the changes
focused on the repo URL / documentation metadata.
[0.1.0] - 2025-12¶
Initial public release. Established the core D38<const SCALE: u32>
type, the strict-vs-fast transcendental dual API, the 256-bit
Möller-Granlund magic-number divide path for mul/div, the
correctly-rounded sqrt / cbrt via exact-integer radicand, the serde
helpers, the d128! macro, and the docs/benchmarks scaffolding.