diff --git a/.gas-snapshot b/.gas-snapshot index e3a596da..af471e8e 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -3,36 +3,37 @@ LibDecimalFloatCompareTest:testCompareOnePointSix() (gas: 5040) LibDecimalFloatCompareTest:testCompareTen() (gas: 5333) LibDecimalFloatDecimalAddTest:testAdd123456789987654321() (gas: 6549) LibDecimalFloatDecimalAddTest:testAdd123456789e9987654321() (gas: 6830) -LibDecimalFloatDecimalAddTest:testAddNeverRevertIsNormalized(int256,int256,int256,int256) (runs: 2063, μ: 14270, ~: 14152) +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: 2063, μ: 6848, ~: 6862) +LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2050, μ: 6849, ~: 6874) LibDecimalFloatDecimalAddTest:testAddZero() (gas: 4388) -LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2063, μ: 10055, ~: 9992) +LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2050, μ: 10061, ~: 9992) LibDecimalFloatDecimalAddTest:testAddZeroOne() (gas: 5256) -LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeExamples() (gas: 22033) -LibDecimalFloatDecimalAddTest:testAddingSmallToLargeReturnsLargeFuzz(int256,int256,int256,int256) (runs: 2054, μ: 18704, ~: 18883) +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: 2063, μ: 10144, ~: 10196) +LibDecimalFloatDecimalTest:testFixedDecimalRoundTripLossless(uint256,uint8) (runs: 2050, μ: 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: 2063, μ: 9574, ~: 7195) -LibDecimalFloatDecimalTest:testFromFixedDecimalLossyTruncateZero(uint256,uint8) (runs: 2063, μ: 8556, ~: 7207) -LibDecimalFloatDecimalTest:testToFixedDecimalLosslessScaleUp(int256,int256,uint8) (runs: 2056, μ: 16000, ~: 16228) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyExponentOverflow(int256,int256,uint8) (runs: 2063, μ: 14938, ~: 14718) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyIdentity(int256,uint8) (runs: 2063, μ: 10110, ~: 9778) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyNegative(int256,int256,uint8) (runs: 2063, μ: 9916, ~: 10161) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyScaleUpOverflow(int256,int256,uint8) (runs: 2055, μ: 13915, ~: 14172) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyTruncate(int256,int256,uint8) (runs: 2063, μ: 14457, ~: 14188) +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:testToFixedDecimalLossyTruncateLossless() (gas: 14500) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyUnderflow(int256,int256,uint8) (runs: 2063, μ: 13700, ~: 13580) -LibDecimalFloatDecimalTest:testToFixedDecimalLossyZero(int256,uint8) (runs: 2063, μ: 4575, ~: 4575) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyUnderflow(int256,int256,uint8) (runs: 2050, μ: 13699, ~: 13580) +LibDecimalFloatDecimalTest:testToFixedDecimalLossyZero(int256,uint8) (runs: 2050, μ: 4575, ~: 4575) LibDecimalFloatDivideTest:testDivide1Over3() (gas: 6388) LibDecimalFloatDivideTest:testDivide1Over3Gas0() (gas: 827) LibDecimalFloatDivideTest:testDivide1Over3Gas10() (gas: 7934) @@ -46,18 +47,21 @@ LibDecimalFloatLog10Test:testExactLogs() (gas: 19667) LibDecimalFloatLog10Test:testInterpolatedLookups() (gas: 18818) LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7926) LibDecimalFloatMultiplyTest:testMultiply123456789987654321() (gas: 4551) -LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2063, μ: 14168, ~: 14254) +LibDecimalFloatMultiplyTest:testMultiply123456789987654321WithExponents(int128,int128) (runs: 2050, μ: 14170, ~: 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: 2063, μ: 3930, ~: 3930) +LibDecimalFloatMultiplyTest:testMultiplyZeroAnyExponent(int64,int64) (runs: 2050, μ: 3930, ~: 3930) LibDecimalFloatMultiplyTest:testMultiplyZeroOne() (gas: 3621) -LibDecimalFloatPackTest:testNormalizedRoundTrip(int256,int256) (runs: 2063, μ: 11025, ~: 10932) -LibDecimalFloatPackTest:testPartsRoundTrip(int128,int128) (runs: 2063, μ: 4588, ~: 4588) +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) 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 cc41bdb1..eb5ba656 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -343,7 +343,11 @@ 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) { - return (signedCoefficientA, exponentA); + if (signedCoefficientA == 0) { + return (signedCoefficientB, exponentB); + } else { + return (signedCoefficientA, exponentA); + } } multiplier = 10 ** alignmentExponentDiff; } @@ -359,6 +363,18 @@ library LibDecimalFloat { return LibDecimalFloatImplementation.normalize(signedCoefficientA, exponentB); } + /// Subtract two floats together as a normalized result. + /// + /// This is effectively shorthand for adding the two floats with the second + /// float negated. Therefore, the same caveats apply as for `add`. + /// @param signedCoefficientA The signed coefficient of the first floating + /// point number. + /// @param exponentA The exponent of the first floating point number. + /// @param signedCoefficientB The signed coefficient of the second floating + /// point number. + /// @param exponentB The exponent of the second floating point number. + /// @return signedCoefficient The signed coefficient of the result. + /// @return exponent The exponent of the result. function sub(int256 signedCoefficientA, int256 exponentA, int256 signedCoefficientB, int256 exponentB) internal pure @@ -368,6 +384,9 @@ library LibDecimalFloat { return add(signedCoefficientA, exponentA, signedCoefficientB, exponentB); } + /// Negates and normalizes a float. + /// Equivalent to `0 - x`. + /// /// https://speleotrove.com/decimal/daops.html#refplusmin /// > minus and plus both take one operand, and correspond to the prefix /// > minus and plus operators in programming languages. @@ -377,6 +396,12 @@ library LibDecimalFloat { /// > (where a and b refer to any numbers) are calculated as the operations /// > add(’0’, a) and subtract(’0’, b) respectively, where the ’0’ has the /// > same exponent as the operand. + /// + /// @param signedCoefficient The signed coefficient of the floating point + /// number. + /// @param exponent The exponent of the floating point number. + /// @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); return (-signedCoefficient, exponent); diff --git a/test/src/LibDecimalFloat.add.t.sol b/test/src/LibDecimalFloat.add.t.sol index 8d43d928..e95e09ce 100644 --- a/test/src/LibDecimalFloat.add.t.sol +++ b/test/src/LibDecimalFloat.add.t.sol @@ -198,4 +198,22 @@ contract LibDecimalFloatDecimalAddTest is Test { 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 { + 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); + (int256 signedCoefficientAddZero, int256 exponentAddZero) = + LibDecimalFloat.add(0, exponentZero, signedCoefficient, exponent); + assertEq(signedCoefficientAddZero, expectedSignedCoefficient); + assertEq(exponentAddZero, expectedExponent); + + // Reverse order. + (signedCoefficientAddZero, exponentAddZero) = LibDecimalFloat.add(signedCoefficient, exponent, 0, exponentZero); + assertEq(signedCoefficientAddZero, expectedSignedCoefficient); + assertEq(exponentAddZero, expectedExponent); + } } diff --git a/test/src/LibDecimalFloat.minus.t.sol b/test/src/LibDecimalFloat.minus.t.sol new file mode 100644 index 00000000..88ed4db3 --- /dev/null +++ b/test/src/LibDecimalFloat.minus.t.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {LibDecimalFloat, EXPONENT_MIN, EXPONENT_MAX} from "src/lib/LibDecimalFloat.sol"; +import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract LibDecimalFloatSubTest is Test { + /// Minus is the same as `0 - x`. + function testMinusIsSubZero(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 signedCoefficientMinus, int256 exponentMinus) = LibDecimalFloat.minus(signedCoefficient, exponent); + (int256 expectedSignedCoefficient, int256 expectedExponent) = + LibDecimalFloat.sub(0, exponentZero, signedCoefficient, exponent); + + assertEq(signedCoefficientMinus, expectedSignedCoefficient); + assertEq(exponentMinus, expectedExponent); + } +} diff --git a/test/src/LibDecimalFloat.sub.t.sol b/test/src/LibDecimalFloat.sub.t.sol new file mode 100644 index 00000000..739453a3 --- /dev/null +++ b/test/src/LibDecimalFloat.sub.t.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: CAL +pragma solidity =0.8.25; + +import {LibDecimalFloat, EXPONENT_MIN, EXPONENT_MAX} from "src/lib/LibDecimalFloat.sol"; +import {LibDecimalFloatImplementation} from "src/lib/implementation/LibDecimalFloatImplementation.sol"; + +import {Test} from "forge-std/Test.sol"; + +contract LibDecimalFloatSubTest is Test { + /// Sub is the same as add, but with the second coefficient negated. + function testSubIsAdd(int256 signedCoefficientA, int256 exponentA, int256 signedCoefficientB, int256 exponentB) + external + pure + { + exponentA = bound(exponentA, EXPONENT_MIN / 10, EXPONENT_MAX / 10); + exponentB = bound(exponentB, EXPONENT_MIN / 10, EXPONENT_MAX / 10); + + // The min signed value cannot be negated directly so we can't test it + // in this function. + vm.assume(signedCoefficientB != type(int256).min); + + (int256 signedCoefficient, int256 exponent) = + LibDecimalFloat.sub(signedCoefficientA, exponentA, signedCoefficientB, exponentB); + (int256 expectedSignedCoefficient, int256 expectedExponent) = + LibDecimalFloat.add(signedCoefficientA, exponentA, -signedCoefficientB, exponentB); + assertEq(signedCoefficient, expectedSignedCoefficient); + assertEq(exponent, expectedExponent); + } + + /// We can sub the min signed value as it will be normalized. + function testSubMinSignedValue(int256 signedCoefficientA, int256 exponentA, int256 exponentB) external pure { + exponentA = bound(exponentA, EXPONENT_MIN / 10, EXPONENT_MAX / 10); + exponentB = bound(exponentB, EXPONENT_MIN / 10, EXPONENT_MAX / 10); + + // Able to sub the non-normalized min signed value. + int256 signedCoefficientB = type(int256).min; + (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); + (int256 expectedSignedCoefficient, int256 expectedExponent) = + LibDecimalFloat.add(signedCoefficientA, exponentA, -signedCoefficientB, exponentB); + + assertEq(signedCoefficient, expectedSignedCoefficient); + assertEq(exponent, expectedExponent); + } +}