Skip to content

Commit

Permalink
Merge pull request #19 from rainlanguage/2024-08-22-lossless
Browse files Browse the repository at this point in the history
lossless conversion
  • Loading branch information
thedavidmeister authored Aug 23, 2024
2 parents c676500 + 09b756a commit 9b63eb7
Show file tree
Hide file tree
Showing 25 changed files with 196 additions and 124 deletions.
160 changes: 82 additions & 78 deletions .gas-snapshot

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/error/ErrDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,11 @@ error Log10Zero();

/// @dev Thrown when attempting to calculate the log of a negative number.
error Log10Negative(int256 signedCoefficient, int256 exponent);

/// @dev Thrown when converting some value to a float when the conversion
/// is lossy.
error LossyConversionToFloat(int256 signedCoefficient, int256 exponent);

/// @dev Thrown when converting a float to some value when the conversion
/// is lossy.
error LossyConversionFromFloat(int256 signedCoefficient, int256 exponent);
77 changes: 44 additions & 33 deletions src/lib/LibDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import {
ANTI_LOG_TABLES_SMALL
} from "../generated/LogTables.pointers.sol";
import {
ExponentOverflow, Log10Negative, Log10Zero, NegativeFixedDecimalConversion
ExponentOverflow,
Log10Negative,
Log10Zero,
NegativeFixedDecimalConversion,
LossyConversionFromFloat,
LossyConversionToFloat
} from "../error/ErrDecimalFloat.sol";
import {
LibDecimalFloatImplementation,
Expand All @@ -19,7 +24,8 @@ import {
EXPONENT_MAX,
NORMALIZED_MIN,
NORMALIZED_MAX,
EXPONENT_STEP_SIZE
EXPONENT_STEP_SIZE,
SIGNED_NORMALIZED_MAX
} from "./implementation/LibDecimalFloatImplementation.sol";

uint256 constant ADD_MAX_EXPONENT_DIFF = 37;
Expand Down Expand Up @@ -80,10 +86,8 @@ int256 constant EXPONENT_LEAP_MULTIPLIER = int256(uint256(10 ** uint256(EXPONENT
/// scope of the typical user of Rainlang.
library LibDecimalFloat {
/// Convert a fixed point decimal value to a signed coefficient and exponent.
/// The returned value will be normalized and the conversion is lossy if this
/// results in a division that causes truncation. This can only happen if the
/// value is greater than `NORMALIZED_MAX`, which is 10^38 - 1. For most use
/// cases, this is not a concern and the conversion will always be lossless.
/// The conversion can be lossy if the unsigned value is too large to fit in
/// the signed coefficient.
/// @param value The fixed point decimal value to convert.
/// @param decimals The number of decimals in the fixed point representation.
/// e.g. If 1e18 represents 1 this would be 18 decimals.
Expand All @@ -98,36 +102,25 @@ library LibDecimalFloat {
// Catch an edge case where unsigned value looks like a negative
// value when coerced.
if (value > uint256(type(int256).max)) {
value /= 10;
exponent += 1;
return (int256(value / 10), exponent + 1, value % 10 == 0);
} else {
return (int256(value), exponent, true);
}
}
}

// Safe to do this conversion of `value` because we've truncated
// anything above `type(int256).max` above by 1 OOM.
(int256 signedCoefficient, int256 finalExponent) =
LibDecimalFloatImplementation.normalize(int256(value), exponent);

return (
signedCoefficient,
finalExponent,
value <= uint256(NORMALIZED_MAX)
// We only hit this if value is greater than NORMALIZED_MAX.
//
// This means that finalExponent is larger than exponent due
// to the normalization. Therefore, we will never attempt to
// cast a negative number to an unsigned number.
//
// It also means that the greatest possible diff between
// value and the normalized value is the difference in OOMs
// between the two due to normalization, which is max at
// rescaling `type(uint256).max`, i.e. ~1.15e77 down to
// ~1.15e37, which is a loss of 40 OOMs. While this is large,
// 40 OOMs is not enough to cause 10 ** 40 to overflow a
// uint256, and we never scale up by more than we first
// scaled down, so we can't overflow the uint256 space.
|| uint256(signedCoefficient) * (10 ** uint256(finalExponent - exponent)) == value
);
/// Lossless version of `fromFixedDecimalLossy`. This will revert if the
/// conversion is lossy.
/// @param value As per `fromFixedDecimalLossy`.
/// @param decimals As per `fromFixedDecimalLossy`.
/// @return signedCoefficient As per `fromFixedDecimalLossy`.
/// @return exponent As per `fromFixedDecimalLossy`.
function fromFixedDecimalLossless(uint256 value, uint8 decimals) internal pure returns (int256, int256) {
(int256 signedCoefficient, int256 exponent, bool lossless) = fromFixedDecimalLossy(value, decimals);
if (!lossless) {
revert LossyConversionToFloat(signedCoefficient, exponent);
}
return (signedCoefficient, exponent);
}

/// Convert a signed coefficient and exponent to a fixed point decimal value.
Expand Down Expand Up @@ -205,6 +198,24 @@ library LibDecimalFloat {
}
}

/// Lossless version of `toFixedDecimalLossy`. This will revert if the
/// conversion is lossy.
/// @param signedCoefficient As per `toFixedDecimalLossy`.
/// @param exponent As per `toFixedDecimalLossy`.
/// @param decimals As per `toFixedDecimalLossy`.
/// @return value As per `toFixedDecimalLossy`.
function toFixedDecimalLossless(int256 signedCoefficient, int256 exponent, uint8 decimals)
internal
pure
returns (uint256)
{
(uint256 value, bool lossless) = toFixedDecimalLossy(signedCoefficient, exponent, decimals);
if (!lossless) {
revert LossyConversionFromFloat(signedCoefficient, exponent);
}
return value;
}

/// Pack a signed coefficient and exponent into a single uint256. Clearly
/// this involves fitting 64 bytes into 32 bytes, so there will be data loss.
/// Normalized numbers are guaranteed to round trip through pack/unpack in
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ contract LibDecimalFloatDecimalTest is Test {
assertEq(signedCoefficient, expectedCoefficient, "signedCoefficient");
assertEq(exponent, expectedExponent, "exponent");
assertEq(lossless, true, "lossless");
assert(LibDecimalFloatImplementation.isNormalized(signedCoefficient, exponent));
}

function checkFromFixedDecimalLossy(
Expand Down Expand Up @@ -74,19 +73,19 @@ contract LibDecimalFloatDecimalTest is Test {

function testFromFixedDecimalLossyOne() external pure {
for (uint8 i = 0; i < type(uint8).max; i++) {
checkFromFixedDecimalLossless(1, i, 1e37, -37 - int256(uint256(i)));
checkFromFixedDecimalLossless(1, i, 1, 0 - int256(uint256(i)));
}
}

function testFromFixedDecimalLossyOneMillion() external pure {
for (uint8 i = 0; i < type(uint8).max; i++) {
checkFromFixedDecimalLossless(1e6, i, 1e37, -37 + 6 - int256(uint256(i)));
checkFromFixedDecimalLossless(1e6, i, 1e6, 0 - int256(uint256(i)));
}
}

function testFromFixedDecimalLossyComplicated() external pure {
for (uint8 i = 0; i < type(uint8).max; i++) {
checkFromFixedDecimalLossless(123456789, i, 1.23456789e37, -37 + 8 - int256(uint256(i)));
checkFromFixedDecimalLossless(123456789, i, 123456789, 0 - int256(uint256(i)));
}
}

Expand All @@ -103,15 +102,15 @@ contract LibDecimalFloatDecimalTest is Test {
/// significant digit is 0.
function testFromFixedDecimalLossyNormalizedMaxPlusOne() external pure {
for (uint8 i = 0; i < type(uint8).max; i++) {
checkFromFixedDecimalLossless(uint256(NORMALIZED_MAX) + 1, i, 1e37, 1 - int256(uint256(i)));
checkFromFixedDecimalLossless(uint256(NORMALIZED_MAX) + 1, i, 1e38, 0 - int256(uint256(i)));
}
}

function testFromFixedDecimalLossyOverflow() external pure {
for (uint8 i = 0; i < type(uint8).max; i++) {
// +2 because this produces a value that actually truncates to
// something different to the original value.
checkFromFixedDecimalLossy(uint256(NORMALIZED_MAX) + 2, i, 1e37, 1 - int256(uint256(i)));
checkFromFixedDecimalLossy(uint256(1e77) + 2, i, 1e76, 1 - int256(uint256(i)));
}
}

Expand Down Expand Up @@ -143,7 +142,7 @@ contract LibDecimalFloatDecimalTest is Test {
// lossy round trip.
uint256 expectedValue = value;
bool expectedLossless = true;
while (expectedValue > uint256(NORMALIZED_MAX)) {
while (expectedValue > uint256(type(int256).max)) {
uint256 nextValue = expectedValue / 10;
if (nextValue * 10 != expectedValue) {
expectedLossless = false;
Expand Down
50 changes: 50 additions & 0 deletions test/src/lib/LibDecimalFloat.decimalLossless.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: CAL
pragma solidity =0.8.25;

import {
LibDecimalFloat,
SIGNED_NORMALIZED_MAX,
NORMALIZED_MAX,
LossyConversionFromFloat,
LossyConversionToFloat
} from "../../../src/lib/LibDecimalFloat.sol";
import {Test} from "forge-std/Test.sol";

contract LibDecimalFloatDecimalLosslessTest is Test {
function fromFixedDecimalLosslessExternal(uint256 value, uint8 decimals) external pure returns (int256, int256) {
return LibDecimalFloat.fromFixedDecimalLossless(value, decimals);
}

function testToFixedDecimalLosslessPass(int256 signedCoefficient, int256 exponent, uint8 decimals) external pure {
signedCoefficient = bound(signedCoefficient, 0, 1e18);
exponent = bound(exponent, 0, 30);
decimals = uint8(bound(decimals, 0, 18));
(uint256 expectedResult, bool success) =
LibDecimalFloat.toFixedDecimalLossy(signedCoefficient, exponent, decimals);
assertTrue(success, "Lossy conversion failed");

uint256 result = LibDecimalFloat.toFixedDecimalLossless(signedCoefficient, exponent, decimals);
assertEq(result, expectedResult, "Lossless conversion failed");
}

function testToFixedDecimalLosslessFail() external {
vm.expectRevert(abi.encodeWithSelector(LossyConversionFromFloat.selector, 1, -1));
LibDecimalFloat.toFixedDecimalLossless(1, -1, 0);
}

function testFromFixedDecimalLosslessPass(uint256 value, uint8 decimals) external pure {
value = bound(value, 0, uint256(type(int256).max));
(int256 signedCoefficient, int256 exponent) = LibDecimalFloat.fromFixedDecimalLossless(value, decimals);
assertEq(uint256(signedCoefficient), value);
assertEq(exponent, -int256(uint256(decimals)));
}

function testFromFixedDecimalLosslessFail(uint256 value, uint8 decimals) external {
value = bound(value, uint256(type(int256).max) + 1, type(uint256).max);
vm.assume(value % 10 != 0);
vm.expectRevert(
abi.encodeWithSelector(LossyConversionToFloat.selector, value / 10, 1 - int256(uint256(decimals)))
);
this.fromFixedDecimalLosslessExternal(value, decimals);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: CAL
pragma solidity =0.8.25;

import {THREES, ONES} from "../lib/LibCommonResults.sol";
import {THREES, ONES} from "../../lib/LibCommonResults.sol";
import {LibDecimalFloat, EXPONENT_MIN, EXPONENT_MAX} from "src/lib/LibDecimalFloat.sol";

import {Test} from "forge-std/Test.sol";
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity =0.8.25;

import {LibDecimalFloat} from "src/lib/LibDecimalFloat.sol";
import {LogTest} from "../abstract/LogTest.sol";
import {LogTest} from "../../abstract/LogTest.sol";

import {console} from "forge-std/Test.sol";

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity =0.8.25;

import {LibDecimalFloat} from "src/lib/LibDecimalFloat.sol";
import {THREES, ONES} from "../lib/LibCommonResults.sol";
import {THREES, ONES} from "../../lib/LibCommonResults.sol";

import {Test} from "forge-std/Test.sol";

Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: CAL
pragma solidity =0.8.25;

import {LogTest} from "../abstract/LogTest.sol";
import {LogTest} from "../../abstract/LogTest.sol";

import {LibDecimalFloat} from "src/lib/LibDecimalFloat.sol";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity =0.8.25;

import {LibDecimalFloat, EXPONENT_MIN} from "src/lib/LibDecimalFloat.sol";
import {LogTest} from "../abstract/LogTest.sol";
import {LogTest} from "../../abstract/LogTest.sol";

import {console2} from "forge-std/Test.sol";

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
EXPONENT_MIN,
EXPONENT_MAX
} from "src/lib/implementation/LibDecimalFloatImplementation.sol";
import {LibDecimalFloatImplementationSlow} from "../../lib/implementation/LibDecimalFloatImplementationSlow.sol";
import {LibDecimalFloatImplementationSlow} from "../../../lib/implementation/LibDecimalFloatImplementationSlow.sol";

contract LibDecimalFloatImplementationNormalizeTest is Test {
/// isNormalized reference.
Expand Down
File renamed without changes.

0 comments on commit 9b63eb7

Please sign in to comment.