dec128 v1.2.0 - Loss Policy And Safe Range


jokruger released dec128 v1.2.0 on 11 September 2026. SetLossPolicy splits overflow, an inexact tail, and underflow to zero off the rounding mode, and the safe operating range is now a tested bound on significant digits. Defaults stay on LossRound, and uint128.FromBigInt no longer mutates its *big.Int argument.

The full release notes and downloads are on the GitHub release page.

SetArithmeticRounding still picks a direction. ROUND_TOWARD_ZERO remains the default. SetLossPolicy decides whether a discarded digit is allowed at all. SetArithmeticRounding(ROUND_NAN) used to do both jobs. It is now the deprecated spelling of SetLossPolicy(LossNaNOnInexact), and it still behaves as it did in v1.1.

Overflow, inexactness, and underflow are separate. An integer part that will not fit at scale 0 is always NaN(Overflow). A result that keeps its significant digits and drops only the tail is inexact: 1/3 and Sqrt(2) are inexact at every scale, and so is a product that needs more than MaxScale (19) places. A result that rounds to zero from nonzero operands is underflow. The smallest nonzero value is 1e-19, so 1e-10 times 1e-10 is 0 under LossRound.

policy              1/3                      1e-10 * 1e-10     1e20 * 1e20
LossRound           0.3333333333333333333    0                 NaN(Overflow)
LossNaNOnUnderflow  0.3333333333333333333    NaN(Underflow)    NaN(Overflow)
LossNaNOnInexact    NaN(Inexact)             NaN(Inexact)      NaN(Overflow)

LossRound is the zero value and the package default. LossNaNOnUnderflow is what the project recommends for money: 1/3 stays a value, and a collapse to zero becomes NaN(Underflow). LossNaNOnInexact refuses every nonzero discarded digit.

SetLossPolicy panics on any other constant, with state.InvalidLossPolicy, and only at configuration time. SetDefaultScale, SetArithmeticRounding, SetLossPolicy, SetTrimOutput, and SetNullValue are process global. Set them once at init. A write while other goroutines calculate is a data race.

SetArithmeticRounding writes the loss policy after the mode. ROUND_NAN stores LossNaNOnInexact. Every other mode stores LossRound. Call SetLossPolicy second or the rounding setter will replace it.

Div and Sqrt call lossState when a quotient loses a digit. A zero quotient means the exact result was below one unit in the last place. fitWide takes the same policy when it shrinks a coefficient wider than 128 bits or a scale above 19. Under LossRound that helper is one comparison.

Export and per call rounding keep their own checks. EncodeIEEE reduces a 35 to 39 digit coefficient to the 34 digit IEEE decimal128 budget, and returns state.Inexact as an error when that drop is nonzero and the policy is LossNaNOnInexact. MulRound, DivRound, and SqrtRound leave lossPolicy unread. A per call mode of ROUND_NAN is what makes them return NaN(Inexact).

A Dec128 carries 38 significant decimal digits, 39 while the coefficient is at or below 340282366920938463463374607431768211455. The digits are shared between the integer part and the fraction, with at most 19 after the decimal point. This tag states that budget in the feature list. The older line was only a scale cap of 19.

TestSafeZoneClaim pins the safe operating range. If every operand has at most 9 decimal places, and every value, intermediate products included, stays below 10^20, then Add, Sub, and Mul are exact.

Mul adds the scales. A product is exact only when s1 + s2 <= 19 and the magnitude fits. Nine plus nine is 18 and stays under MaxScale. Ten plus ten is 20, so a uniform scale of 10 rounds every product. Add and Sub do not raise the scale. Div and Sqrt run at DefaultScale(), 19 unless changed, and are exact only when the result terminates in that many digits. 1/3 has 19 significant digits.

All 19 fractional places exist while the absolute value is at most 34028236692093846346.3374607431768211455. That integer part is about 3.4e19, 3.69 times the largest int64. Above it, one fractional place drops per decade, down to none near 3.4e38. The floor is 1e-19. FromString("1e-20") returns NaN(ScaleOutOfRange). Under LossRound, arithmetic below that floor returns zero.

FromString("0.000000001").Mul(FromString("0.000000001"))    // exact, 1e-18
FromString("0.0000000001").Mul(FromString("0.0000000001"))  // 0, because 1e-20 does not fit

At two decimal places the top is about 3.40e36, many orders of magnitude past any real balance. Rates and per unit factors near 1e-10 hit the floor. Keep them in one expression, rescale into a larger unit, or set LossNaNOnUnderflow. MaxAtScale(scale) is uint128.Max at that scale. QuantumAtScale(scale) is one unit in the last place. A scale above MaxScale yields NaN(ScaleOutOfRange) from both.

Before this tag, uint128.FromBigInt returned Uint128{i.Uint64(), i.Rsh(i, 64).Uint64()}. (*big.Int).Rsh writes into its receiver. The low 64 bits were copied, then the caller’s *big.Int was shifted right by 64 bits so the high limb could be read. Later use of that pointer saw a different number.

v1.2.0 shifts a new integer and leaves the argument as it was:

Uint128{i.Uint64(), new(big.Int).Rsh(i, 64).Uint64()}

Status codes are the same. Nil returns zero and state.OK. A negative value returns zero and state.NegativeInUnsignedOp. A value above 2^128-1 returns state.Overflow.

Defaults are unchanged, including a silent zero on underflow.

SetArithmeticRounding overwrites lossPolicy on every call. Set SetLossPolicy after it, once, before arithmetic starts. Mul, Div, and Sqrt take no per value policy. An undefined policy panics at init.