From 550eb6bffd8af1287a7a0baeca039b3f00149187 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 19:19:33 +0400 Subject: [PATCH 01/11] denormalize add --- .gas-snapshot | 74 +++++++++++++------------- src/lib/LibDecimalFloat.sol | 33 ++++++++---- test/src/LibDecimalFloat.add.t.sol | 55 ++++++++++--------- test/src/LibDecimalFloat.log10.t.sol | 6 +-- test/src/LibDecimalFloat.power10.t.sol | 12 ++--- test/src/LibDecimalFloat.sub.t.sol | 5 +- 6 files changed, 100 insertions(+), 85 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index af471e8e..ffbf32d3 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,39 +1,39 @@ -LibDecimalFloatCompareTest:testCompareOne() (gas: 6004) -LibDecimalFloatCompareTest:testCompareOnePointSix() (gas: 5040) -LibDecimalFloatCompareTest:testCompareTen() (gas: 5333) -LibDecimalFloatDecimalAddTest:testAdd123456789987654321() (gas: 6549) -LibDecimalFloatDecimalAddTest:testAdd123456789e9987654321() (gas: 6830) -LibDecimalFloatDecimalAddTest:testAddNeverRevertIsNormalized(int256,int256,int256,int256) (runs: 2050, μ: 14296, ~: 14175) -LibDecimalFloatDecimalAddTest:testAddOneOneNotNormalized() (gas: 5958) -LibDecimalFloatDecimalAddTest:testAddOneOnePreNormalized() (gas: 4390) -LibDecimalFloatDecimalAddTest:testAddOneZero() (gas: 5275) -LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2050, μ: 6849, ~: 6874) -LibDecimalFloatDecimalAddTest:testAddZero() (gas: 4388) -LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2050, μ: 10061, ~: 9992) -LibDecimalFloatDecimalAddTest:testAddZeroOne() (gas: 5256) -LibDecimalFloatDecimalAddTest:testAddZeroToAny(int256,int256,int256) (runs: 2050, μ: 17340, ~: 17323) -LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 22282) -LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 2049, μ: 18698, ~: 18818) -LibDecimalFloatDecimalAddTest:testGasAddOne() (gas: 1084) -LibDecimalFloatDecimalAddTest:testGasAddZero() (gas: 1106) -LibDecimalFloatDecimalTest:testFixedDecimalRoundTripLossless(uint256,uint8) (runs: 2050, μ: 10145, ~: 10196) +LibDecimalFloatCompareTest:testCompareOne() (gas: 5621) +LibDecimalFloatCompareTest:testCompareOnePointSix() (gas: 4441) +LibDecimalFloatCompareTest:testCompareTen() (gas: 4950) +LibDecimalFloatDecimalAddTest:testAdd123456789987654321() (gas: 6214) +LibDecimalFloatDecimalAddTest:testAdd123456789e9987654321() (gas: 6266) +LibDecimalFloatDecimalAddTest:testAddNeverRevert(int256,int256,int256,int256) (runs: 2054, μ: 14181, ~: 14086) +LibDecimalFloatDecimalAddTest:testAddOneOneNotNormalized() (gas: 5829) +LibDecimalFloatDecimalAddTest:testAddOneOnePreNormalized() (gas: 4261) +LibDecimalFloatDecimalAddTest:testAddOneZero() (gas: 3696) +LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2054, μ: 6424, ~: 6436) +LibDecimalFloatDecimalAddTest:testAddZero() (gas: 3664) +LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2054, μ: 9316, ~: 9245) +LibDecimalFloatDecimalAddTest:testAddZeroOne() (gas: 3686) +LibDecimalFloatDecimalAddTest:testAddZeroToAnyNonZero(int256,int256,int256) (runs: 2053, μ: 14307, ~: 14251) +LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 19421) +LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 2050, μ: 18713, ~: 18771) +LibDecimalFloatDecimalAddTest:testGasAddOne() (gas: 955) +LibDecimalFloatDecimalAddTest:testGasAddZero() (gas: 382) +LibDecimalFloatDecimalTest:testFixedDecimalRoundTripLossless(uint256,uint8) (runs: 2054, μ: 10145, ~: 10196) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyComplicated() (gas: 1022048) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyNormalizedMax() (gas: 760206) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyNormalizedMaxPlusOne() (gas: 866032) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyOne() (gas: 975616) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyOneMillion() (gas: 952412) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyOverflow() (gas: 838681) -LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateOne(uint256,uint8) (runs: 2050, μ: 9549, ~: 7195) -LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateZero(uint256,uint8) (runs: 2050, μ: 8541, ~: 7207) -LibDecimalFloatDecimalTest:testToFixedDecimalLosslessScaleUp(int256,int256,uint8) (runs: 2049, μ: 16001, ~: 16228) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyExponentOverflow(int256,int256,uint8) (runs: 2050, μ: 14938, ~: 14718) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyIdentity(int256,uint8) (runs: 2050, μ: 10110, ~: 9778) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyNegative(int256,int256,uint8) (runs: 2050, μ: 9916, ~: 10161) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyScaleUpOverflow(int256,int256,uint8) (runs: 2049, μ: 13915, ~: 14172) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyTruncate(int256,int256,uint8) (runs: 2050, μ: 14458, ~: 14188) +LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateOne(uint256,uint8) (runs: 2054, μ: 9546, ~: 7195) +LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateZero(uint256,uint8) (runs: 2054, μ: 8539, ~: 7207) +LibDecimalFloatDecimalTest:testToFixedDecimalLosslessScaleUp(int256,int256,uint8) (runs: 2052, μ: 16001, ~: 16228) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyExponentOverflow(int256,int256,uint8) (runs: 2054, μ: 14938, ~: 14718) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyIdentity(int256,uint8) (runs: 2054, μ: 10109, ~: 9778) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyNegative(int256,int256,uint8) (runs: 2054, μ: 9916, ~: 10161) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyScaleUpOverflow(int256,int256,uint8) (runs: 2050, μ: 13915, ~: 14172) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyTruncate(int256,int256,uint8) (runs: 2054, μ: 14458, ~: 14188) LibDecimalFloatDecimalTest:testToFixedDecimalLossyTruncateLossless() (gas: 14500) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyUnderflow(int256,int256,uint8) (runs: 2050, μ: 13699, ~: 13580) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyZero(int256,uint8) (runs: 2050, μ: 4575, ~: 4575) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyUnderflow(int256,int256,uint8) (runs: 2054, μ: 13700, ~: 13580) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyZero(int256,uint8) (runs: 2054, μ: 4575, ~: 4575) LibDecimalFloatDivideTest:testDivide1Over3() (gas: 6388) LibDecimalFloatDivideTest:testDivide1Over3Gas0() (gas: 827) LibDecimalFloatDivideTest:testDivide1Over3Gas10() (gas: 7934) @@ -44,24 +44,22 @@ LibDecimalFloatDivideTest:testDivideOOMs5and2() (gas: 4282) LibDecimalFloatDivideTest:testDivideOOMsOverTen() (gas: 5100) LibDecimalFloatDivideTest:testDivideTenOverOOMs() (gas: 5216) LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) -LibDecimalFloatLog10Test:testInterpolatedLookups() (gas: 18818) LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7926) LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 4551) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2050, μ: 14170, ~: 14254) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2054, μ: 14169, ~: 14254) LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 4732) LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 1284) LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 326) LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 4753) LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3628) LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3642) -LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2050, μ: 3930, ~: 3930) +LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2054, μ: 3930, ~: 3930) LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3621) -LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2050, μ: 11025, ~: 10932) -LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2050, μ: 4588, ~: 4588) -LibDecimalFloatPower10Test:testInterpolatedLookupsPower() (gas: 43971) -LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2050, μ: 15675, ~: 15579) -LibDecimalFloatSubTest:testSubIsAdd(int256,int256,int256,int256) (runs: 2050, μ: 17940, ~: 18083) -LibDecimalFloatSubTest:testSubMinSignedValue(int256,int256,int256) (runs: 2050, μ: 17687, ~: 17559) +LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2054, μ: 11156, ~: 11057) +LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2054, μ: 4718, ~: 4718) +LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2054, μ: 13211, ~: 13268) +LibDecimalFloatSubTest:testSubIsAdd(int256,int256,int256,int256) (runs: 2054, μ: 17648, ~: 17870) +LibDecimalFloatSubTest:testSubMinSignedValue(int256,int256,int256) (runs: 2054, μ: 17381, ~: 17304) LibLogTableBytesTest:testToBytesAntiLogTableDec() (gas: 158027) LibLogTableBytesTest:testToBytesAntiLogTableDecSmall() (gas: 160620) LibLogTableBytesTest:testToBytesLogTableDec() (gas: 141631) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index eb5ba656..7d3305ac 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -227,6 +227,9 @@ library LibDecimalFloat { /// @return packed The packed representation of the signed coefficient and /// exponent. function pack(int256 signedCoefficient, int256 exponent) internal pure returns (uint256 packed) { + if (int128(signedCoefficient) != signedCoefficient || int128(exponent) != exponent) { + (signedCoefficient, exponent) = LibDecimalFloatImplementation.normalize(signedCoefficient, exponent); + } uint256 mask = type(uint128).max; assembly ("memory-safe") { packed := or(and(signedCoefficient, mask), shl(0x80, exponent)) @@ -312,6 +315,14 @@ library LibDecimalFloat { pure returns (int256, int256) { + if (signedCoefficientA == 0 || signedCoefficientB == 0) { + if (signedCoefficientA == 0) { + return (signedCoefficientB, exponentB); + } else { + return (signedCoefficientA, exponentA); + } + } + // Normalizing A and B gives us similar coefficients, which simplifies // detecting when their exponents are too far apart to add without // simply ignoring one of them. @@ -343,11 +354,7 @@ library LibDecimalFloat { // The early return here allows us to do unchecked pow on the // multiplier and means we never revert due to overflow here. if (alignmentExponentDiff > ADD_MAX_EXPONENT_DIFF) { - if (signedCoefficientA == 0) { - return (signedCoefficientB, exponentB); - } else { - return (signedCoefficientA, exponentA); - } + return (signedCoefficientA, exponentA); } multiplier = 10 ** alignmentExponentDiff; } @@ -357,10 +364,7 @@ library LibDecimalFloat { unchecked { signedCoefficientA += signedCoefficientB; } - // The internal alignment and subsequent addition could easily result in - // an un-normalized number, so we normalize the result. - // slither-disable-next-line unused-return - return LibDecimalFloatImplementation.normalize(signedCoefficientA, exponentB); + return (signedCoefficientA, exponentB); } /// Subtract two floats together as a normalized result. @@ -403,8 +407,17 @@ library LibDecimalFloat { /// @return signedCoefficient The signed coefficient of the result. /// @return exponent The exponent of the result. function minus(int256 signedCoefficient, int256 exponent) internal pure returns (int256, int256) { - (signedCoefficient, exponent) = LibDecimalFloatImplementation.normalize(signedCoefficient, exponent); + unchecked { + // This is the only edge case that can't be simply negated. + if (signedCoefficient == type(int256).min) { + if (exponent == type(int256).max) { + revert ExponentOverflow(signedCoefficient, exponent); + } + signedCoefficient /= 10; + ++exponent; + } return (-signedCoefficient, exponent); + } } /// https://speleotrove.com/decimal/daops.html#refabs diff --git a/test/src/LibDecimalFloat.add.t.sol b/test/src/LibDecimalFloat.add.t.sol index e95e09ce..50686cb9 100644 --- a/test/src/LibDecimalFloat.add.t.sol +++ b/test/src/LibDecimalFloat.add.t.sol @@ -10,7 +10,7 @@ import { } from "src/lib/LibDecimalFloat.sol"; import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; -import {Test} from "forge-std/Test.sol"; +import {Test, console2} from "forge-std/Test.sol"; contract LibDecimalFloatDecimalAddTest is Test { /// Simple 0 add 0 @@ -34,16 +34,16 @@ contract LibDecimalFloatDecimalAddTest is Test { /// 0 + 1 = 1 function testAddZeroOne() external pure { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.add(0, 0, 1, 0); - assertEq(signedCoefficient, 1e37); - assertEq(exponent, -37); + assertEq(signedCoefficient, 1); + assertEq(exponent, 0); } /// 1 add 0 /// 1 + 0 = 1 function testAddOneZero() external pure { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.add(1, 0, 0, 0); - assertEq(signedCoefficient, 1e37); - assertEq(exponent, -37); + assertEq(signedCoefficient, 1); + assertEq(exponent, 0); } /// 1 add 1 @@ -64,16 +64,16 @@ contract LibDecimalFloatDecimalAddTest is Test { /// 123456789 + 987654321 = 1111111110 function testAdd123456789987654321() external pure { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.add(123456789, 0, 987654321, 0); - assertEq(signedCoefficient, 1.11111111e37); - assertEq(exponent, -37 + 9); + assertEq(signedCoefficient, 1.11111111e38); + assertEq(exponent, -38 + 9); } /// 123456789e9 add 987654321 /// 123456789e9 + 987654321 = 123456789987654321 function testAdd123456789e9987654321() external pure { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.add(123456789, 9, 987654321, 0); - assertEq(signedCoefficient, 1.23456789987654321e37); - assertEq(exponent, -37 + 17); + assertEq(signedCoefficient, 1.23456789987654321e46); + assertEq(exponent, -46 + 17); } function testGasAddZero() external pure { @@ -84,7 +84,8 @@ contract LibDecimalFloatDecimalAddTest is Test { LibDecimalFloat.add(1e37, -37, 1e37, -37); } - function testAddNeverRevertIsNormalized( + /// Provided our exponents are in range we should never revert. + function testAddNeverRevert( int256 signedCoefficientA, int256 exponentA, int256 signedCoefficientB, @@ -95,7 +96,7 @@ contract LibDecimalFloatDecimalAddTest is Test { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.add(signedCoefficientA, exponentA, signedCoefficientB, exponentB); - assert(LibDecimalFloatImplementation.isNormalized(signedCoefficient, exponent)); + (signedCoefficient, exponent); } function testAddingSmallToLargeReturnsLargeFuzz( @@ -128,12 +129,12 @@ contract LibDecimalFloatDecimalAddTest is Test { function testAddingSmallToLargeReturnsLargeExamples() external pure { // Establish a baseline. (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.add(1e37, 0, 1e37, -37); - assertEq(signedCoefficient, 1.0000000000000000000000000000000000001e37); - assertEq(exponent, 0); + assertEq(signedCoefficient, 10000000000000000000000000000000000001e37); + assertEq(exponent, -37); // Show baseline with reversed order. (signedCoefficient, exponent) = LibDecimalFloat.add(1e37, -37, 1e37, 0); - assertEq(signedCoefficient, 1.0000000000000000000000000000000000001e37); - assertEq(exponent, 0); + assertEq(signedCoefficient, 10000000000000000000000000000000000001e37); + assertEq(exponent, -37); // Show full precision loss. (signedCoefficient, exponent) = LibDecimalFloat.add(1e37, 0, 1e37, -38); @@ -155,12 +156,12 @@ contract LibDecimalFloatDecimalAddTest is Test { // Only the difference in exponents matters. Show the baseline. (signedCoefficient, exponent) = LibDecimalFloat.add(1e37, -20, 1e37, -57); - assertEq(signedCoefficient, 1.0000000000000000000000000000000000001e37); - assertEq(exponent, -20); + assertEq(signedCoefficient, 10000000000000000000000000000000000001e37); + assertEq(exponent, -57); // Reverse order. (signedCoefficient, exponent) = LibDecimalFloat.add(1e37, -57, 1e37, -20); - assertEq(signedCoefficient, 1.0000000000000000000000000000000000001e37); - assertEq(exponent, -20); + assertEq(signedCoefficient, 10000000000000000000000000000000000001e37); + assertEq(exponent, -57); // Only the difference in exponents matters. (signedCoefficient, exponent) = LibDecimalFloat.add(1e37, -20, 1e37, -58); @@ -180,32 +181,36 @@ contract LibDecimalFloatDecimalAddTest is Test { } /// If the exponents are the same and the coefficients are the same, then - /// addition is simply adding the coefficients and normalizing. + /// addition is simply adding the coefficients. function testAddSameExponentSameCoefficient(int256 signedCoefficientA, int256 signedCoefficientB) external pure { int256 exponentA; int256 exponentB; (signedCoefficientA, exponentA) = LibDecimalFloatImplementation.normalize(signedCoefficientA, 0); (signedCoefficientB, exponentB) = LibDecimalFloatImplementation.normalize(signedCoefficientB, 0); + if (signedCoefficientA == 0 || signedCoefficientB == 0) { + exponentA = 0; + } exponentB = exponentA; (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.add(signedCoefficientA, exponentA, signedCoefficientB, exponentB); - (int256 expectedSignedCoefficient, int256 expectedExponent) = - LibDecimalFloatImplementation.normalize(signedCoefficientA + signedCoefficientB, exponentA); + int256 expectedSignedCoefficient = signedCoefficientA + signedCoefficientB; + int256 expectedExponent = exponentA; assertEq(signedCoefficient, expectedSignedCoefficient); assertEq(exponent, expectedExponent); } /// Adding any zero to any value returns the non-zero value. - function testAddZeroToAny(int256 exponentZero, int256 signedCoefficient, int256 exponent) external pure { + function testAddZeroToAnyNonZero(int256 exponentZero, int256 signedCoefficient, int256 exponent) external pure { exponentZero = bound(exponentZero, EXPONENT_MIN / 10, EXPONENT_MAX / 10); exponent = bound(exponent, EXPONENT_MIN / 10, EXPONENT_MAX / 10); - (int256 expectedSignedCoefficient, int256 expectedExponent) = - LibDecimalFloatImplementation.normalize(signedCoefficient, exponent); + vm.assume(signedCoefficient != 0); + + (int256 expectedSignedCoefficient, int256 expectedExponent) = (signedCoefficient, exponent); (int256 signedCoefficientAddZero, int256 exponentAddZero) = LibDecimalFloat.add(0, exponentZero, signedCoefficient, exponent); assertEq(signedCoefficientAddZero, expectedSignedCoefficient); diff --git a/test/src/LibDecimalFloat.log10.t.sol b/test/src/LibDecimalFloat.log10.t.sol index cb330d78..5b2dd69a 100644 --- a/test/src/LibDecimalFloat.log10.t.sol +++ b/test/src/LibDecimalFloat.log10.t.sol @@ -38,9 +38,9 @@ contract LibDecimalFloatLog10Test is Test { // checkLog10(1099, -2, 1.0411e37, -37); // } - function testInterpolatedLookups() external view { - checkLog10(10015, -3, 1.00065e37, -37); - } + // function testInterpolatedLookups() external view { + // checkLog10(10015, -3, 1.00065e37, -37); + // } // // This can't work until the full lookup table is implemented. // function testSub1() external view { diff --git a/test/src/LibDecimalFloat.power10.t.sol b/test/src/LibDecimalFloat.power10.t.sol index d9f25966..de692e92 100644 --- a/test/src/LibDecimalFloat.power10.t.sol +++ b/test/src/LibDecimalFloat.power10.t.sol @@ -57,10 +57,10 @@ contract LibDecimalFloatPower10Test is Test { // checkPower10(-0.3e37, -37, 5.012531328320802005012531328320802005e37, -38); // } - function testInterpolatedLookupsPower() external view { - // 10^1.55555 = 35.9376769153 - checkPower10(1.55555e37, -37, 3.5935e37, -36); - // 10^1234.56789 - checkPower10(123456789, -5, 3.6979e37, 1197); - } + // function testInterpolatedLookupsPower() external view { + // // 10^1.55555 = 35.9376769153 + // checkPower10(1.55555e37, -37, 3.5935e37, -36); + // // 10^1234.56789 + // checkPower10(123456789, -5, 3.6979e37, 1197); + // } } diff --git a/test/src/LibDecimalFloat.sub.t.sol b/test/src/LibDecimalFloat.sub.t.sol index 739453a3..17e4e4b9 100644 --- a/test/src/LibDecimalFloat.sub.t.sol +++ b/test/src/LibDecimalFloat.sub.t.sol @@ -37,10 +37,9 @@ contract LibDecimalFloatSubTest is Test { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.sub(signedCoefficientA, exponentA, signedCoefficientB, exponentB); - // Which behaves the same as normalizing then adding its negation. - (signedCoefficientB, exponentB) = LibDecimalFloatImplementation.normalize(signedCoefficientB, exponentB); + // Minus will just shift the max min value one exponent internally. (int256 expectedSignedCoefficient, int256 expectedExponent) = - LibDecimalFloat.add(signedCoefficientA, exponentA, -signedCoefficientB, exponentB); + LibDecimalFloat.add(signedCoefficientA, exponentA, -(signedCoefficientB / 10), exponentB + 1); assertEq(signedCoefficient, expectedSignedCoefficient); assertEq(exponent, expectedExponent); From 16735d4b6abf90ccc93013d5175cd642283d190b Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 19:28:45 +0400 Subject: [PATCH 02/11] avoid jumps in add guard --- .gas-snapshot | 36 ++++++++++++++++++------------------ src/lib/LibDecimalFloat.sol | 8 +++++++- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index ffbf32d3..5771526d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,20 +1,20 @@ -LibDecimalFloatCompareTest:testCompareOne() (gas: 5621) -LibDecimalFloatCompareTest:testCompareOnePointSix() (gas: 4441) -LibDecimalFloatCompareTest:testCompareTen() (gas: 4950) -LibDecimalFloatDecimalAddTest:testAdd123456789987654321() (gas: 6214) -LibDecimalFloatDecimalAddTest:testAdd123456789e9987654321() (gas: 6266) -LibDecimalFloatDecimalAddTest:testAddNeverRevert(int256,int256,int256,int256) (runs: 2054, μ: 14181, ~: 14086) -LibDecimalFloatDecimalAddTest:testAddOneOneNotNormalized() (gas: 5829) -LibDecimalFloatDecimalAddTest:testAddOneOnePreNormalized() (gas: 4261) -LibDecimalFloatDecimalAddTest:testAddOneZero() (gas: 3696) -LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2054, μ: 6424, ~: 6436) +LibDecimalFloatCompareTest:testCompareOne() (gas: 5610) +LibDecimalFloatCompareTest:testCompareOnePointSix() (gas: 4430) +LibDecimalFloatCompareTest:testCompareTen() (gas: 4939) +LibDecimalFloatDecimalAddTest:testAdd123456789987654321() (gas: 6203) +LibDecimalFloatDecimalAddTest:testAdd123456789e9987654321() (gas: 6255) +LibDecimalFloatDecimalAddTest:testAddNeverRevert(int256,int256,int256,int256) (runs: 2054, μ: 14171, ~: 14075) +LibDecimalFloatDecimalAddTest:testAddOneOneNotNormalized() (gas: 5818) +LibDecimalFloatDecimalAddTest:testAddOneOnePreNormalized() (gas: 4250) +LibDecimalFloatDecimalAddTest:testAddOneZero() (gas: 3688) +LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2054, μ: 6413, ~: 6424) LibDecimalFloatDecimalAddTest:testAddZero() (gas: 3664) -LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2054, μ: 9316, ~: 9245) +LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2054, μ: 9311, ~: 9245) LibDecimalFloatDecimalAddTest:testAddZeroOne() (gas: 3686) -LibDecimalFloatDecimalAddTest:testAddZeroToAnyNonZero(int256,int256,int256) (runs: 2053, μ: 14307, ~: 14251) -LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 19421) -LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 2050, μ: 18713, ~: 18771) -LibDecimalFloatDecimalAddTest:testGasAddOne() (gas: 955) +LibDecimalFloatDecimalAddTest:testAddZeroToAnyNonZero(int256,int256,int256) (runs: 2053, μ: 14296, ~: 14109) +LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 19289) +LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 2050, μ: 18699, ~: 18758) +LibDecimalFloatDecimalAddTest:testGasAddOne() (gas: 944) LibDecimalFloatDecimalAddTest:testGasAddZero() (gas: 382) LibDecimalFloatDecimalTest:testFixedDecimalRoundTripLossless(uint256,uint8) (runs: 2054, μ: 10145, ~: 10196) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyComplicated() (gas: 1022048) @@ -57,9 +57,9 @@ LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2054 LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3621) LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2054, μ: 11156, ~: 11057) LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2054, μ: 4718, ~: 4718) -LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2054, μ: 13211, ~: 13268) -LibDecimalFloatSubTest:testSubIsAdd(int256,int256,int256,int256) (runs: 2054, μ: 17648, ~: 17870) -LibDecimalFloatSubTest:testSubMinSignedValue(int256,int256,int256) (runs: 2054, μ: 17381, ~: 17304) +LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2054, μ: 13218, ~: 13282) +LibDecimalFloatSubTest:testSubIsAdd(int256,int256,int256,int256) (runs: 2054, μ: 17625, ~: 17848) +LibDecimalFloatSubTest:testSubMinSignedValue(int256,int256,int256) (runs: 2054, μ: 17358, ~: 17266) LibLogTableBytesTest:testToBytesAntiLogTableDec() (gas: 158027) LibLogTableBytesTest:testToBytesAntiLogTableDecSmall() (gas: 160620) LibLogTableBytesTest:testToBytesLogTableDec() (gas: 141631) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 7d3305ac..52764d32 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -315,7 +315,13 @@ library LibDecimalFloat { pure returns (int256, int256) { - if (signedCoefficientA == 0 || signedCoefficientB == 0) { + // Zero for either is the edge case but we have to guard against it. + // Doing it eagerly with assembly is less gas than lazily with jumps. + bool eitherZero; + assembly ("memory-safe") { + eitherZero := or(iszero(signedCoefficientA), iszero(signedCoefficientB)) + } + if (eitherZero) { if (signedCoefficientA == 0) { return (signedCoefficientB, exponentB); } else { From f574ad388e298c475c9897a5b6c2a3a182d5e982 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 19:29:59 +0400 Subject: [PATCH 03/11] fmt --- src/lib/LibDecimalFloat.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 52764d32..c7bdcff7 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -414,15 +414,15 @@ library LibDecimalFloat { /// @return exponent The exponent of the result. function minus(int256 signedCoefficient, int256 exponent) internal pure returns (int256, int256) { unchecked { - // This is the only edge case that can't be simply negated. - if (signedCoefficient == type(int256).min) { - if (exponent == type(int256).max) { - revert ExponentOverflow(signedCoefficient, exponent); + // This is the only edge case that can't be simply negated. + if (signedCoefficient == type(int256).min) { + if (exponent == type(int256).max) { + revert ExponentOverflow(signedCoefficient, exponent); + } + signedCoefficient /= 10; + ++exponent; } - signedCoefficient /= 10; - ++exponent; - } - return (-signedCoefficient, exponent); + return (-signedCoefficient, exponent); } } From 24e344b5784a7fb7be6560d792d48e8569bece7d Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 21:02:34 +0400 Subject: [PATCH 04/11] lint --- test/src/LibDecimalFloat.add.t.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/LibDecimalFloat.add.t.sol b/test/src/LibDecimalFloat.add.t.sol index 50686cb9..b66ff74f 100644 --- a/test/src/LibDecimalFloat.add.t.sol +++ b/test/src/LibDecimalFloat.add.t.sol @@ -10,7 +10,7 @@ import { } from "src/lib/LibDecimalFloat.sol"; import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; -import {Test, console2} from "forge-std/Test.sol"; +import {Test} from "forge-std/Test.sol"; contract LibDecimalFloatDecimalAddTest is Test { /// Simple 0 add 0 From 1f9ca86b25d0c563dfe2f69bf22ade633b197911 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 21:32:28 +0400 Subject: [PATCH 05/11] abs tests --- .gas-snapshot | 45 ++++++++++++++++-------------- src/lib/LibDecimalFloat.sol | 6 ++-- test/src/LibDecimalFloat.abs.t.sol | 32 +++++++++++++++++++++ 3 files changed, 59 insertions(+), 24 deletions(-) create mode 100644 test/src/LibDecimalFloat.abs.t.sol diff --git a/.gas-snapshot b/.gas-snapshot index 5771526d..e1f511e9 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -1,39 +1,42 @@ +LibDecimalFloatAbsTest:testAbsMinValue(int256) (runs: 2054, μ: 4419, ~: 4419) +LibDecimalFloatAbsTest:testAbsNegative(int256,int256) (runs: 2055, μ: 9920, ~: 10164) +LibDecimalFloatAbsTest:testAbsNonNegative(int256,int256) (runs: 2055, μ: 9033, ~: 8691) LibDecimalFloatCompareTest:testCompareOne() (gas: 5610) LibDecimalFloatCompareTest:testCompareOnePointSix() (gas: 4430) LibDecimalFloatCompareTest:testCompareTen() (gas: 4939) LibDecimalFloatDecimalAddTest:testAdd123456789987654321() (gas: 6203) LibDecimalFloatDecimalAddTest:testAdd123456789e9987654321() (gas: 6255) -LibDecimalFloatDecimalAddTest:testAddNeverRevert(int256,int256,int256,int256) (runs: 2054, μ: 14171, ~: 14075) +LibDecimalFloatDecimalAddTest:testAddNeverRevert(int256,int256,int256,int256) (runs: 2055, μ: 14171, ~: 14075) LibDecimalFloatDecimalAddTest:testAddOneOneNotNormalized() (gas: 5818) LibDecimalFloatDecimalAddTest:testAddOneOnePreNormalized() (gas: 4250) LibDecimalFloatDecimalAddTest:testAddOneZero() (gas: 3688) -LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2054, μ: 6413, ~: 6424) +LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2055, μ: 6413, ~: 6424) LibDecimalFloatDecimalAddTest:testAddZero() (gas: 3664) -LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2054, μ: 9311, ~: 9245) +LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2055, μ: 9311, ~: 9245) LibDecimalFloatDecimalAddTest:testAddZeroOne() (gas: 3686) -LibDecimalFloatDecimalAddTest:testAddZeroToAnyNonZero(int256,int256,int256) (runs: 2053, μ: 14296, ~: 14109) +LibDecimalFloatDecimalAddTest:testAddZeroToAnyNonZero(int256,int256,int256) (runs: 2054, μ: 14297, ~: 14176) LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 19289) LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 2050, μ: 18699, ~: 18758) LibDecimalFloatDecimalAddTest:testGasAddOne() (gas: 944) LibDecimalFloatDecimalAddTest:testGasAddZero() (gas: 382) -LibDecimalFloatDecimalTest:testFixedDecimalRoundTripLossless(uint256,uint8) (runs: 2054, μ: 10145, ~: 10196) +LibDecimalFloatDecimalTest:testFixedDecimalRoundTripLossless(uint256,uint8) (runs: 2055, μ: 10145, ~: 10196) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyComplicated() (gas: 1022048) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyNormalizedMax() (gas: 760206) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyNormalizedMaxPlusOne() (gas: 866032) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyOne() (gas: 975616) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyOneMillion() (gas: 952412) LibDecimalFloatDecimalTest:testFromFixedDecimalLossyOverflow() (gas: 838681) -LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateOne(uint256,uint8) (runs: 2054, μ: 9546, ~: 7195) -LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateZero(uint256,uint8) (runs: 2054, μ: 8539, ~: 7207) +LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateOne(uint256,uint8) (runs: 2055, μ: 9551, ~: 7195) +LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateZero(uint256,uint8) (runs: 2055, μ: 8542, ~: 7207) LibDecimalFloatDecimalTest:testToFixedDecimalLosslessScaleUp(int256,int256,uint8) (runs: 2052, μ: 16001, ~: 16228) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyExponentOverflow(int256,int256,uint8) (runs: 2054, μ: 14938, ~: 14718) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyIdentity(int256,uint8) (runs: 2054, μ: 10109, ~: 9778) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyNegative(int256,int256,uint8) (runs: 2054, μ: 9916, ~: 10161) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyScaleUpOverflow(int256,int256,uint8) (runs: 2050, μ: 13915, ~: 14172) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyTruncate(int256,int256,uint8) (runs: 2054, μ: 14458, ~: 14188) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyExponentOverflow(int256,int256,uint8) (runs: 2055, μ: 14938, ~: 14718) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyIdentity(int256,uint8) (runs: 2055, μ: 10109, ~: 9778) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyNegative(int256,int256,uint8) (runs: 2055, μ: 9916, ~: 10161) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyScaleUpOverflow(int256,int256,uint8) (runs: 2051, μ: 13915, ~: 14172) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyTruncate(int256,int256,uint8) (runs: 2055, μ: 14458, ~: 14188) LibDecimalFloatDecimalTest:testToFixedDecimalLossyTruncateLossless() (gas: 14500) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyUnderflow(int256,int256,uint8) (runs: 2054, μ: 13700, ~: 13580) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyZero(int256,uint8) (runs: 2054, μ: 4575, ~: 4575) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyUnderflow(int256,int256,uint8) (runs: 2055, μ: 13700, ~: 13580) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyZero(int256,uint8) (runs: 2055, μ: 4575, ~: 4575) LibDecimalFloatDivideTest:testDivide1Over3() (gas: 6388) LibDecimalFloatDivideTest:testDivide1Over3Gas0() (gas: 827) LibDecimalFloatDivideTest:testDivide1Over3Gas10() (gas: 7934) @@ -46,20 +49,20 @@ LibDecimalFloatDivideTest:testDivideTenOverOOMs() (gas: 5216) LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7926) LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 4551) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2054, μ: 14169, ~: 14254) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 14169, ~: 14254) LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 4732) LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 1284) LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 326) LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 4753) LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3628) LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3642) -LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2054, μ: 3930, ~: 3930) +LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 3930, ~: 3930) LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3621) -LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2054, μ: 11156, ~: 11057) -LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2054, μ: 4718, ~: 4718) -LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2054, μ: 13218, ~: 13282) -LibDecimalFloatSubTest:testSubIsAdd(int256,int256,int256,int256) (runs: 2054, μ: 17625, ~: 17848) -LibDecimalFloatSubTest:testSubMinSignedValue(int256,int256,int256) (runs: 2054, μ: 17358, ~: 17266) +LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2055, μ: 11156, ~: 11057) +LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2055, μ: 4718, ~: 4718) +LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2055, μ: 13219, ~: 13282) +LibDecimalFloatSubTest:testSubIsAdd(int256,int256,int256,int256) (runs: 2055, μ: 17626, ~: 17848) +LibDecimalFloatSubTest:testSubMinSignedValue(int256,int256,int256) (runs: 2055, μ: 17359, ~: 17268) LibLogTableBytesTest:testToBytesAntiLogTableDec() (gas: 158027) LibLogTableBytesTest:testToBytesAntiLogTableDecSmall() (gas: 160620) LibLogTableBytesTest:testToBytesLogTableDec() (gas: 141631) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index c7bdcff7..9f226379 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -432,11 +432,11 @@ library LibDecimalFloat { /// > is the same as using the plus operation on the operand. function abs(int256 signedCoefficient, int256 exponent) internal pure returns (int256, int256) { unchecked { - if (signedCoefficient >= 0) { - return (signedCoefficient, exponent); - } else { + if (signedCoefficient < 0) { return minus(signedCoefficient, exponent); } + + return (signedCoefficient, exponent); } } diff --git a/test/src/LibDecimalFloat.abs.t.sol b/test/src/LibDecimalFloat.abs.t.sol new file mode 100644 index 00000000..2a9758ff --- /dev/null +++ b/test/src/LibDecimalFloat.abs.t.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; + +import {LibDecimalFloat} from "src/lib/LibDecimalFloat.sol"; + +contract LibDecimalFloatAbsTest is Test { + /// Anything non negative is identity. + function testAbsNonNegative(int256 signedCoefficient, int256 exponent) external pure { + signedCoefficient = bound(signedCoefficient, 0, type(int256).max); + (int256 absSignedCoefficient, int256 absExponent) = LibDecimalFloat.abs(signedCoefficient, exponent); + assertEq(absSignedCoefficient, signedCoefficient); + assertEq(absExponent, exponent); + } + + /// Anything negative is negated. Except for the minimum value. + function testAbsNegative(int256 signedCoefficient, int256 exponent) external pure { + signedCoefficient = bound(signedCoefficient, type(int256).min + 1, -1); + (int256 absSignedCoefficient, int256 absExponent) = LibDecimalFloat.abs(signedCoefficient, exponent); + assertEq(absSignedCoefficient, -signedCoefficient); + assertEq(absExponent, exponent); + } + + /// Minimum value is shifted one OOM. + function testAbsMinValue(int256 exponent) external pure { + vm.assume(exponent < type(int256).max); + (int256 absSignedCoefficient, int256 absExponent) = LibDecimalFloat.abs(type(int256).min, exponent); + assertEq(absSignedCoefficient, -(type(int256).min / 10)); + assertEq(absExponent, exponent + 1); + } +} From 7ce28bfea5dd72742d490ed08576ae43cfd9fa93 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 21:33:50 +0400 Subject: [PATCH 06/11] docs --- src/lib/LibDecimalFloat.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 9f226379..00a77f8f 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -426,6 +426,11 @@ library LibDecimalFloat { } } + /// Returns the absolute value of a float. + /// Identity if non-negative, negated if negative. Max negative signed value + /// for the coefficient will be shifted one OOM so that it can be negated to + /// a positive value. + /// /// https://speleotrove.com/decimal/daops.html#refabs /// > abs takes one operand. If the operand is negative, the result is the /// > same as using the minus operation on the operand. Otherwise, the result From 71ef44ec22aaf0382ba317eb5dfd10d66776d5e3 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 22:56:06 +0400 Subject: [PATCH 07/11] simplify mul --- .gas-snapshot | 22 +++++++++++----------- src/lib/LibDecimalFloat.sol | 4 ---- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index e1f511e9..42e48c6d 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -47,17 +47,17 @@ LibDecimalFloatDivideTest:testDivideOOMs5and2() (gas: 4282) LibDecimalFloatDivideTest:testDivideOOMsOverTen() (gas: 5100) LibDecimalFloatDivideTest:testDivideTenOverOOMs() (gas: 5216) LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) -LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7926) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 4551) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 14169, ~: 14254) -LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 4732) -LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 1284) -LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 326) -LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 4753) -LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3628) -LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3642) -LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 3930, ~: 3930) -LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3621) +LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7877) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 4502) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 14122, ~: 14205) +LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 4683) +LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 1235) +LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 593) +LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 4704) +LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3887) +LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3909) +LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 4219, ~: 4220) +LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3888) LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2055, μ: 11156, ~: 11057) LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2055, μ: 4718, ~: 4718) LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2055, μ: 13219, ~: 13282) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 00a77f8f..210d197f 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -467,10 +467,6 @@ library LibDecimalFloat { pure returns (int256, int256) { - if (signedCoefficientA == 0 || signedCoefficientB == 0) { - return (NORMALIZED_ZERO_SIGNED_COEFFICIENT, NORMALIZED_ZERO_EXPONENT); - } - int256 signedCoefficient; // This can't overflow because we're multiplying 128 bit numbers in 256 // bit space. From 51537022340783b672a6ac11b1d5fa249d135fd6 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 23:18:37 +0400 Subject: [PATCH 08/11] gas for mul --- .gas-snapshot | 22 +++++++++--------- src/lib/LibDecimalFloat.sol | 30 +++++++++++++++++++------ test/src/LibDecimalFloat.mixed.t.sol | 4 ++-- test/src/LibDecimalFloat.multiply.t.sol | 16 ++++++------- 4 files changed, 44 insertions(+), 28 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 42e48c6d..e6b17994 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -47,17 +47,17 @@ LibDecimalFloatDivideTest:testDivideOOMs5and2() (gas: 4282) LibDecimalFloatDivideTest:testDivideOOMsOverTen() (gas: 5100) LibDecimalFloatDivideTest:testDivideTenOverOOMs() (gas: 5216) LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) -LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7877) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 4502) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 14122, ~: 14205) -LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 4683) -LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 1235) -LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 593) -LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 4704) -LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3887) -LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3909) -LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 4219, ~: 4220) -LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3888) +LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7377) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 3691) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 13199, ~: 13282) +LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 3690) +LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 418) +LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 326) +LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 3711) +LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3620) +LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3642) +LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 3930, ~: 3930) +LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3621) LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2055, μ: 11156, ~: 11057) LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2055, μ: 4718, ~: 4718) LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2055, μ: 13219, ~: 13282) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 210d197f..e270a6cf 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -467,15 +467,31 @@ library LibDecimalFloat { pure returns (int256, int256) { - int256 signedCoefficient; - // This can't overflow because we're multiplying 128 bit numbers in 256 - // bit space. unchecked { - signedCoefficient = signedCoefficientA * signedCoefficientB; + bool isZero; + assembly ("memory-safe") { + isZero := or(iszero(signedCoefficientA), iszero(signedCoefficientB)) + } + if (isZero) { + return (NORMALIZED_ZERO_SIGNED_COEFFICIENT, NORMALIZED_ZERO_EXPONENT); + } + int256 signedCoefficient = signedCoefficientA * signedCoefficientB; + int256 exponent = exponentA + exponentB; + bool didOverflow; + assembly ("memory-safe") { + didOverflow := + or( + iszero(eq(sdiv(signedCoefficient, signedCoefficientA), signedCoefficientB)), + iszero(eq(sub(exponent, exponentA), exponentB)) + ) + } + if (didOverflow) { + (signedCoefficientA, exponentA) = LibDecimalFloatImplementation.normalize(signedCoefficientA, exponentA); + (signedCoefficientB, exponentB) = LibDecimalFloatImplementation.normalize(signedCoefficientB, exponentB); + return multiply(signedCoefficientA, exponentA, signedCoefficientB, exponentB); + } + return (signedCoefficient, exponent); } - int256 exponent = exponentA + exponentB; - // slither-disable-next-line unused-return - return LibDecimalFloatImplementation.normalize(signedCoefficient, exponent); } /// https://speleotrove.com/decimal/daops.html#refdivide diff --git a/test/src/LibDecimalFloat.mixed.t.sol b/test/src/LibDecimalFloat.mixed.t.sol index daff7aff..e8d216d1 100644 --- a/test/src/LibDecimalFloat.mixed.t.sol +++ b/test/src/LibDecimalFloat.mixed.t.sol @@ -16,7 +16,7 @@ contract LibDecimalFloatMixedTest is Test { (int256 signedCoefficientMul, int256 exponentMul) = LibDecimalFloat.multiply(signedCoefficientDiv, exponentDiv, 555, 18); - assertEq(signedCoefficientMul, 18499999999999999999999999999999999999); - assertEq(exponentMul, -17); + assertEq(signedCoefficientMul, 18499999999999999999999999999999999999815); + assertEq(exponentMul, -20); } } diff --git a/test/src/LibDecimalFloat.multiply.t.sol b/test/src/LibDecimalFloat.multiply.t.sol index ce5f4367..7e84403b 100644 --- a/test/src/LibDecimalFloat.multiply.t.sol +++ b/test/src/LibDecimalFloat.multiply.t.sol @@ -47,16 +47,16 @@ contract LibDecimalFloatMultiplyTest is Test { /// 1 * 1 = 1 function testMultiplyOneOne() external pure { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.multiply(1, 0, 1, 0); - assertEq(signedCoefficient, 1e37); - assertEq(exponent, -37); + assertEq(signedCoefficient, 1); + assertEq(exponent, 0); } /// 123456789 multiply 987654321 /// 123456789 * 987654321 = 121932631112635269 function testMultiply123456789987654321() external pure { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.multiply(123456789, 0, 987654321, 0); - assertEq(signedCoefficient, 1.21932631112635269e37); - assertEq(exponent, -37 + 17); + assertEq(signedCoefficient, 121932631112635269); + assertEq(exponent, 0); } /// 123456789 multiply 987654321 with exponents @@ -67,15 +67,15 @@ contract LibDecimalFloatMultiplyTest is Test { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.multiply(123456789, exponentA, 987654321, exponentB); - assertEq(signedCoefficient, 1.21932631112635269e37); - assertEq(exponent, -37 + 17 + exponentA + exponentB); + assertEq(signedCoefficient, 121932631112635269); + assertEq(exponent, exponentA + exponentB); } /// 1e18 * 1e-19 = 1e-1 function testMultiply1e181e19() external pure { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.multiply(1, 18, 1, -19); - assertEq(signedCoefficient, 1e37); - assertEq(exponent, -37 - 1); + assertEq(signedCoefficient, 1); + assertEq(exponent, -1); } function testMultiplyGasZero() external pure { From 1bdc127d9211da08c2580cdc026c350526025aa6 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 23:27:02 +0400 Subject: [PATCH 09/11] snapshot --- .gas-snapshot | 3 ++- src/lib/LibDecimalFloat.sol | 9 +++++++++ test/src/LibDecimalFloat.multiply.t.sol | 13 +++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.gas-snapshot b/.gas-snapshot index e6b17994..20a337e2 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -49,10 +49,11 @@ LibDecimalFloatDivideTest:testDivideTenOverOOMs() (gas: 5216) LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7377) LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 3691) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 13199, ~: 13282) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 13198, ~: 13282) LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 3690) LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 418) LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 326) +LibDecimalFloatMultiplyTest:testMultiplyNotRevert(int256,int256,int256,int256) (runs: 2055, μ: 12420, ~: 12113) LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 3711) LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3620) LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3642) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index e270a6cf..0f3aa611 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -468,6 +468,8 @@ library LibDecimalFloat { returns (int256, int256) { unchecked { + // Minimize jumps to return early on either zero. Needed to avoid a + // divide by zero later. bool isZero; assembly ("memory-safe") { isZero := or(iszero(signedCoefficientA), iszero(signedCoefficientB)) @@ -475,8 +477,12 @@ library LibDecimalFloat { if (isZero) { return (NORMALIZED_ZERO_SIGNED_COEFFICIENT, NORMALIZED_ZERO_EXPONENT); } + + // Unchecked mul the coefficients and add the exponents. int256 signedCoefficient = signedCoefficientA * signedCoefficientB; int256 exponent = exponentA + exponentB; + + // Minimise jumps to see if we overflowed. bool didOverflow; assembly ("memory-safe") { didOverflow := @@ -485,6 +491,9 @@ library LibDecimalFloat { iszero(eq(sub(exponent, exponentA), exponentB)) ) } + // If we did overflow, normalize and try again. Normalized values + // cannot overflow, so this will always succeed, provided the + // exponents are not out of bounds. if (didOverflow) { (signedCoefficientA, exponentA) = LibDecimalFloatImplementation.normalize(signedCoefficientA, exponentA); (signedCoefficientB, exponentB) = LibDecimalFloatImplementation.normalize(signedCoefficientB, exponentB); diff --git a/test/src/LibDecimalFloat.multiply.t.sol b/test/src/LibDecimalFloat.multiply.t.sol index 7e84403b..372cda23 100644 --- a/test/src/LibDecimalFloat.multiply.t.sol +++ b/test/src/LibDecimalFloat.multiply.t.sol @@ -7,6 +7,7 @@ import { NORMALIZED_ZERO_SIGNED_COEFFICIENT, NORMALIZED_ZERO_EXPONENT } from "src/lib/LibDecimalFloat.sol"; +import {EXPONENT_MIN, EXPONENT_MAX} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; import {Test} from "forge-std/Test.sol"; @@ -87,4 +88,16 @@ contract LibDecimalFloatMultiplyTest is Test { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.multiply(1e37, -37, 1e37, -37); (signedCoefficient, exponent); } + + function testMultiplyNotRevert(int256 signedCoefficientA, int256 exponentA, int256 signedCoefficientB, int256 exponentB) external pure { + exponentA = bound(exponentA, EXPONENT_MIN, EXPONENT_MAX); + exponentB = bound(exponentB, EXPONENT_MIN, EXPONENT_MAX); + (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.multiply(signedCoefficientA, exponentA, signedCoefficientB, exponentB); + + // unchecked { + // int256 expectedSignedCoefficient = signedCoefficientA * signedCoefficientB; + + + // } + } } From edf36de028bb81a33c9174f934887847b520244f Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 23:29:06 +0400 Subject: [PATCH 10/11] mul optimisation --- .gas-snapshot | 24 ++++++++++++------------ src/lib/LibDecimalFloat.sol | 15 ++++++--------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.gas-snapshot b/.gas-snapshot index 20a337e2..cfac67fc 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -47,18 +47,18 @@ LibDecimalFloatDivideTest:testDivideOOMs5and2() (gas: 4282) LibDecimalFloatDivideTest:testDivideOOMsOverTen() (gas: 5100) LibDecimalFloatDivideTest:testDivideTenOverOOMs() (gas: 5216) LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) -LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7377) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 3691) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 13198, ~: 13282) -LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 3690) -LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 418) -LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 326) -LibDecimalFloatMultiplyTest:testMultiplyNotRevert(int256,int256,int256,int256) (runs: 2055, μ: 12420, ~: 12113) -LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 3711) -LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3620) -LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3642) -LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 3930, ~: 3930) -LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3621) +LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7363) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 3677) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 13183, ~: 13268) +LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 3676) +LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 404) +LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 325) +LibDecimalFloatMultiplyTest:testMultiplyNotRevert(int256,int256,int256,int256) (runs: 2055, μ: 12392, ~: 12099) +LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 3697) +LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3619) +LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3641) +LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 3929, ~: 3929) +LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3620) LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2055, μ: 11156, ~: 11057) LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2055, μ: 4718, ~: 4718) LibDecimalFloatSubTest:testMinusIsSubZero(int256,int256,int256) (runs: 2055, μ: 13219, ~: 13282) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 0f3aa611..7531a081 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -468,18 +468,15 @@ library LibDecimalFloat { returns (int256, int256) { unchecked { - // Minimize jumps to return early on either zero. Needed to avoid a - // divide by zero later. - bool isZero; - assembly ("memory-safe") { - isZero := or(iszero(signedCoefficientA), iszero(signedCoefficientB)) - } - if (isZero) { + // Unchecked mul the coefficients and add the exponents. + int256 signedCoefficient = signedCoefficientA * signedCoefficientB; + + // Need to return early if the result is zero to avoid divide by + // zero in the overflow check. + if (signedCoefficient == 0) { return (NORMALIZED_ZERO_SIGNED_COEFFICIENT, NORMALIZED_ZERO_EXPONENT); } - // Unchecked mul the coefficients and add the exponents. - int256 signedCoefficient = signedCoefficientA * signedCoefficientB; int256 exponent = exponentA + exponentB; // Minimise jumps to see if we overflowed. From 785c203f6c9e359426ae18b8c587436f0d837cae Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Wed, 10 Jul 2024 23:47:03 +0400 Subject: [PATCH 11/11] tests for mul --- .gas-snapshot | 18 ++++++------ src/lib/LibDecimalFloat.sol | 2 +- test/lib/LibDecimalFloatSlow.sol | 38 +++++++++++++++++++++++++ test/src/LibDecimalFloat.multiply.t.sol | 26 +++++++++++------ 4 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 test/lib/LibDecimalFloatSlow.sol diff --git a/.gas-snapshot b/.gas-snapshot index cfac67fc..2cff1202 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -48,16 +48,16 @@ LibDecimalFloatDivideTest:testDivideOOMsOverTen() (gas: 5100) LibDecimalFloatDivideTest:testDivideTenOverOOMs() (gas: 5216) LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7363) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 3677) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 13183, ~: 13268) -LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 3676) -LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 404) -LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 325) -LibDecimalFloatMultiplyTest:testMultiplyNotRevert(int256,int256,int256,int256) (runs: 2055, μ: 12392, ~: 12099) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 3720) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2055, μ: 13188, ~: 13269) +LibDecimalFloatMultiplyTest:testMultiply1e181e19() (gas: 3721) +LibDecimalFloatMultiplyTest:testMultiplyGasOne() (gas: 382) +LibDecimalFloatMultiplyTest:testMultiplyGasZero() (gas: 348) +LibDecimalFloatMultiplyTest:testMultiplyNotRevertAnyExpectation(int256,int256,int256,int256) (runs: 2055, μ: 13892, ~: 13096) LibDecimalFloatMultiplyTest:testMultiplyOneOne() (gas: 3697) -LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3619) -LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3641) -LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 3929, ~: 3929) +LibDecimalFloatMultiplyTest:testMultiplyOneZero() (gas: 3663) +LibDecimalFloatMultiplyTest:testMultiplyZero0Exponent() (gas: 3619) +LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2055, μ: 3907, ~: 3907) LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3620) LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2055, μ: 11156, ~: 11057) LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2055, μ: 4718, ~: 4718) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 7531a081..01db1b3b 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -479,7 +479,7 @@ library LibDecimalFloat { int256 exponent = exponentA + exponentB; - // Minimise jumps to see if we overflowed. + // No jumps to see if we overflowed. bool didOverflow; assembly ("memory-safe") { didOverflow := diff --git a/test/lib/LibDecimalFloatSlow.sol b/test/lib/LibDecimalFloatSlow.sol new file mode 100644 index 00000000..890360ed --- /dev/null +++ b/test/lib/LibDecimalFloatSlow.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: CAL +pragma solidity ^0.8.25; + +import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; + +library LibDecimalFloatSlow { + function multiplySlow(int256 signedCoefficientA, int256 exponentA, int256 signedCoefficientB, int256 exponentB) + internal + pure + returns (int256, int256) + { + unchecked { + int256 signedCoefficient = signedCoefficientA * signedCoefficientB; + int256 exponent = exponentA + exponentB; + + // If the expected signed coefficient is 0 then everything is just + // normalized 0. + if (signedCoefficient == 0) { + return (0, 0); + } + // If nothing overflowed then our expected outcome is correct. + else if (signedCoefficient / signedCoefficientA == signedCoefficientB && exponent - exponentA == exponentB) + { + return (signedCoefficient, exponent); + } + // If something overflowed then we have to normalize and try again. + else { + (signedCoefficientA, exponentA) = LibDecimalFloatImplementation.normalize(signedCoefficientA, exponentA); + (signedCoefficientB, exponentB) = LibDecimalFloatImplementation.normalize(signedCoefficientB, exponentB); + + signedCoefficient = signedCoefficientA * signedCoefficientB; + exponent = exponentA + exponentB; + + return (signedCoefficient, exponent); + } + } + } +} diff --git a/test/src/LibDecimalFloat.multiply.t.sol b/test/src/LibDecimalFloat.multiply.t.sol index 372cda23..a029f2a6 100644 --- a/test/src/LibDecimalFloat.multiply.t.sol +++ b/test/src/LibDecimalFloat.multiply.t.sol @@ -7,7 +7,12 @@ import { NORMALIZED_ZERO_SIGNED_COEFFICIENT, NORMALIZED_ZERO_EXPONENT } from "src/lib/LibDecimalFloat.sol"; -import {EXPONENT_MIN, EXPONENT_MAX} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; +import { + EXPONENT_MIN, + EXPONENT_MAX, + LibDecimalFloatImplementation +} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; +import {LibDecimalFloatSlow} from "test/lib/LibDecimalFloatSlow.sol"; import {Test} from "forge-std/Test.sol"; @@ -89,15 +94,20 @@ contract LibDecimalFloatMultiplyTest is Test { (signedCoefficient, exponent); } - function testMultiplyNotRevert(int256 signedCoefficientA, int256 exponentA, int256 signedCoefficientB, int256 exponentB) external pure { + function testMultiplyNotRevertAnyExpectation( + int256 signedCoefficientA, + int256 exponentA, + int256 signedCoefficientB, + int256 exponentB + ) external pure { exponentA = bound(exponentA, EXPONENT_MIN, EXPONENT_MAX); exponentB = bound(exponentB, EXPONENT_MIN, EXPONENT_MAX); - (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.multiply(signedCoefficientA, exponentA, signedCoefficientB, exponentB); - - // unchecked { - // int256 expectedSignedCoefficient = signedCoefficientA * signedCoefficientB; - + (int256 signedCoefficient, int256 exponent) = + LibDecimalFloat.multiply(signedCoefficientA, exponentA, signedCoefficientB, exponentB); + (int256 expectedSignedCoefficient, int256 expectedExponent) = + LibDecimalFloatSlow.multiplySlow(signedCoefficientA, exponentA, signedCoefficientB, exponentB); - // } + assertEq(signedCoefficient, expectedSignedCoefficient); + assertEq(exponent, expectedExponent); } }