From 9cb0b50bae1113c2ed4ecca18135d51a7b6641c7 Mon Sep 17 00:00:00 2001 From: David Meister Date: Sat, 13 Jun 2026 08:00:14 +0000 Subject: [PATCH 1/2] test: exercise adjustExponent logic paths in div The `div` implementation selects an `adjustExponent` for scaling the numerator via a binary search over the order of magnitude of the (maximized) divisor coefficient. The smaller-than-1e75 leaves of that search were not exercised by the existing tests, because they are only reachable when the divisor cannot be maximized (its exponent is pinned at type(int256).min, leaving the coefficient at its given magnitude). Adds deterministic unit tests covering: - every interior leaf of the binary search (one divisor strictly inside each sub-range, 1e5 .. 1e75) plus the "noop" leaf, - the strict `<` boundary comparisons at each power-of-ten boundary, - the dedicated full-divisor (scale 1e75) and >=1e76 (scale 1e76) paths, - the exponent-adjustment application paths: applied to exponentA, spilled over onto exponentB, the spill-overflow return-zero case, and the underflow return-zero case. Each case asserts the round-trip identity `(a / b) * b == a` (compared as a value via `eq`), which pins both the quotient mantissa and the `adjustExponent` constant for that branch: a wrong constant scales the quotient by a power of ten and breaks the equality. Confirmed discriminating by mutating an `adjustExponent` constant and observing the new test fail. Test-only change; no production source is modified. Co-Authored-By: Claude Opus 4.8 --- .../LibDecimalFloatImplementation.div.t.sol | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol b/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol index 3a757e5..5623467 100644 --- a/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol +++ b/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol @@ -199,4 +199,119 @@ contract LibDecimalFloatImplementationDivTest is Test { ++di; } } + + /// Asserts the round trip identity `(a / b) * b == a` (as a value, via `eq`) + /// for exact divisions. This pins both the quotient mantissa AND the exponent + /// bookkeeping (including the `adjustExponent` constant selected for `b`), + /// because a wrong `adjustExponent` would scale the quotient by a power of + /// ten and break the equality. + function checkDivInverse(int256 signedCoefficientA, int256 exponentA, int256 signedCoefficientB, int256 exponentB) + internal + pure + { + (int256 q, int256 qe) = + LibDecimalFloatImplementation.div(signedCoefficientA, exponentA, signedCoefficientB, exponentB); + (int256 back, int256 backE) = LibDecimalFloatImplementation.mul(q, qe, signedCoefficientB, exponentB); + assertTrue(LibDecimalFloatImplementation.eq(back, backE, signedCoefficientA, exponentA), "(a / b) * b == a"); + } + + /// The scaling logic inside `div` selects an `adjustExponent` based on a + /// binary search over the order of magnitude of the (maximized) divisor + /// coefficient. The smaller-than-`1e75` leaves of that search are only + /// reachable when the divisor cannot be maximized because its exponent sits + /// at `type(int256).min`, leaving the coefficient at its given magnitude. + /// + /// Each row below places `9 * 10^j / 3 * 10^j` (an exact `3`) with the + /// divisor pinned to `type(int256).min` so that `3 * 10^j` lands strictly + /// inside one sub-range of the binary search. The quotient mantissa is + /// therefore always `3e76` and the round trip must hold. + function testDivAdjustExponentLeaves() external pure { + int256 min = type(int256).min; + // [div by, lands in sub-range) + int256[16] memory divisors = [ + int256(3), // < 1e5 + 3e6, // [1e5, 1e10) + 3e11, // [1e10, 1e14) + 3e15, // [1e14, 1e19) + 3e20, // [1e19, 1e23) + 3e24, // [1e23, 1e28) + 3e29, // [1e28, 1e33) + 3e34, // [1e33, 1e38) + 3e39, // [1e38, 1e43) + 3e44, // [1e43, 1e48) + 3e49, // [1e48, 1e53) + 3e54, // [1e53, 1e58) + 3e59, // [1e58, 1e63) + 3e64, // [1e63, 1e68) + 3e69, // [1e68, 1e73) + 3e73 // [1e73, 1e75) the "noop" leaf that keeps the starting 1e76 scale + ]; + for (uint256 i = 0; i < divisors.length; i++) { + int256 numerator = 3 * divisors[i]; + (int256 q, int256 qe) = LibDecimalFloatImplementation.div(numerator, 0, divisors[i], min); + // A single significant figure "3" maximizes to 76 digits, i.e. 3e75. + assertEq(q, 3e75, "quotient mantissa"); + // The exponent is enormous (close to -type(int256).min) so it is + // pinned by the round trip rather than a literal here. + (int256 back, int256 backE) = LibDecimalFloatImplementation.mul(q, qe, divisors[i], min); + assertTrue(LibDecimalFloatImplementation.eq(back, backE, numerator, 0), "round trip"); + } + } + + /// Each binary search boundary is `< scale` (strict), so a divisor sitting + /// exactly on a power-of-ten boundary falls into the higher sub-range. This + /// exercises the boundary comparisons themselves rather than the interiors. + function testDivAdjustExponentBoundaries() external pure { + int256 min = type(int256).min; + int256[14] memory boundaries = + [int256(1e5), 1e10, 1e14, 1e19, 1e23, 1e28, 1e33, 1e38, 1e43, 1e48, 1e53, 1e58, 1e63, 1e68]; + for (uint256 i = 0; i < boundaries.length; i++) { + // A divisor of exactly 10^k divides any 10^m numerator exactly. + checkDivInverse(boundaries[i], 0, boundaries[i], min); + } + } + + /// When the maximized divisor coefficient is full (>= 1e75) but still less + /// than 1e76, `div` takes the dedicated `scale = 1e75` / `adjustExponent = 75` + /// branch rather than the binary search. + function testDivAdjustExponentFullDivisor() external pure { + int256 min = type(int256).min; + // 3e75 has 76 digits, so 3e75 / 1e75 == 3 != 0 => "full", but 3e75 < 1e76. + (int256 q, int256 qe) = LibDecimalFloatImplementation.div(9e75, 0, 3e75, min); + assertEq(q, 3e75, "full divisor quotient mantissa"); + (int256 back, int256 backE) = LibDecimalFloatImplementation.mul(q, qe, 3e75, min); + assertTrue(LibDecimalFloatImplementation.eq(back, backE, 9e75, 0), "full divisor round trip"); + } + + /// When the maximized divisor coefficient is already >= 1e76 the whole + /// scaling block is skipped and the starting `adjustExponent = 76` is used. + function testDivAdjustExponentLargeDivisor() external pure { + int256 min = type(int256).min; + // 3e76 >= 1e76 so the `if (signedCoefficientBAbs < scale)` block is skipped. + checkDivInverse(3e76, 0, 3e76, min); + } + + /// The exponent adjustment is first applied to `exponentA`. When `exponentA` + /// is already at `type(int256).min` the leftover adjustment spills over onto + /// `exponentB` instead. + function testDivAdjustExponentSpillsToExponentB() external pure { + int256 min = type(int256).min; + // 1e76 is full at any exponent, so fullA holds even at min, avoiding the + // MaximizeOverflow revert while forcing the spill-to-exponentB path. + // 1e76 * 10^min / (3e75 * 10^min) == 10/3. + checkDiv(1e76, min, 3e75, min, THREES, -75); + } + + /// When the adjustment cannot be applied to `exponentA` (already at the + /// minimum) and applying the remainder to `exponentB` would overflow it past + /// `type(int256).max`, `div` returns maximized zero. + function testDivAdjustExponentSpillOverflowReturnsZero() external pure { + checkDiv(1e76, type(int256).min, 3e75, type(int256).max, 0, 0); + } + + /// A division whose true exponent underflows below what a single result can + /// represent returns maximized zero. + function testDivUnderflowReturnsZero() external pure { + checkDiv(1e76, type(int256).min, 3, type(int256).max, 0, 0); + } } From a64dd7d45f388df441ca64f218793968b5255551 Mon Sep 17 00:00:00 2001 From: David Meister Date: Sat, 13 Jun 2026 08:01:56 +0000 Subject: [PATCH 2/2] test: clarify quotient mantissa comment in adjustExponent leaves Co-Authored-By: Claude Opus 4.8 --- .../lib/implementation/LibDecimalFloatImplementation.div.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol b/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol index 5623467..d844073 100644 --- a/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol +++ b/test/src/lib/implementation/LibDecimalFloatImplementation.div.t.sol @@ -224,7 +224,8 @@ contract LibDecimalFloatImplementationDivTest is Test { /// Each row below places `9 * 10^j / 3 * 10^j` (an exact `3`) with the /// divisor pinned to `type(int256).min` so that `3 * 10^j` lands strictly /// inside one sub-range of the binary search. The quotient mantissa is - /// therefore always `3e76` and the round trip must hold. + /// therefore always the maximized `3` (i.e. `3e75`) and the round trip must + /// hold regardless of which sub-range was selected. function testDivAdjustExponentLeaves() external pure { int256 min = type(int256).min; // [div by, lands in sub-range)