Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 49 additions & 47 deletions .gas-snapshot
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
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)
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: 2055, μ: 14171, ~: 14075)
LibDecimalFloatDecimalAddTest:testAddOneOneNotNormalized() (gas: 5818)
LibDecimalFloatDecimalAddTest:testAddOneOnePreNormalized() (gas: 4250)
LibDecimalFloatDecimalAddTest:testAddOneZero() (gas: 3688)
LibDecimalFloatDecimalAddTest:testAddSameExponentSameCoefficient(int256,int256) (runs: 2055, μ: 6413, ~: 6424)
LibDecimalFloatDecimalAddTest:testAddZero() (gas: 3664)
LibDecimalFloatDecimalAddTest:testAddZeroAnyExponent(int128) (runs: 2055, μ: 9311, ~: 9245)
LibDecimalFloatDecimalAddTest:testAddZeroOne() (gas: 3686)
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: 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: 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: 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: 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: 2050, μ: 13699, ~: 13580)
LibDecimalFloatDecimalTest:testToFixedDecimalLossyZero(int256,uint8) (runs: 2050, μ: 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)
Expand All @@ -44,24 +47,23 @@ 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: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: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)
LibDecimalFloatMixedTest:testDivide1Over3() (gas: 7363)
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: 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)
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)
Expand Down
92 changes: 67 additions & 25 deletions src/lib/LibDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -312,6 +315,20 @@ library LibDecimalFloat {
pure
returns (int256, int256)
{
// 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 {
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.
Expand Down Expand Up @@ -343,11 +360,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;
}
Expand All @@ -357,10 +370,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.
Expand Down Expand Up @@ -403,21 +413,35 @@ 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);
return (-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);
}
}

/// 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
/// > 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);
}
}

Expand All @@ -443,19 +467,37 @@ 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.
unchecked {
signedCoefficient = signedCoefficientA * signedCoefficientB;
// 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);
}

int256 exponent = exponentA + exponentB;

// No jumps to see if we overflowed.
bool didOverflow;
assembly ("memory-safe") {
didOverflow :=
or(
iszero(eq(sdiv(signedCoefficient, signedCoefficientA), signedCoefficientB)),
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);
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
Expand Down
38 changes: 38 additions & 0 deletions test/lib/LibDecimalFloatSlow.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
32 changes: 32 additions & 0 deletions test/src/LibDecimalFloat.abs.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading