diff --git a/.gas-snapshot b/.gas-snapshot index efbd2636..8f05dfc3 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -178,12 +178,14 @@ LibDecimalFloatPowerTest:testRoundTrip() (gas: 1343164) LibDecimalFloatSubTest:testSubIsAdd(int256,int256,int256,int256) (runs: 5099, μ: 15312, ~: 15201) LibDecimalFloatSubTest:testSubMem((int256,int256),(int256,int256)) (runs: 5099, μ: 8998, ~: 9242) LibDecimalFloatSubTest:testSubMinSignedValue(int256,int256,int256) (runs: 5099, μ: 15556, ~: 15464) +LibFormatDecimalFloatTest:testFormatMem((int256,int256)) (runs: 5099, μ: 6030, ~: 6688) +LibFormatDecimalFloatTest:testRoundTrip(uint256) (runs: 5099, μ: 23970, ~: 18699) LibLogTableBytesTest:testToBytesAntiLogTableDec() (gas: 153225) LibLogTableBytesTest:testToBytesAntiLogTableDecSmall() (gas: 158036) LibLogTableBytesTest:testToBytesLogTableDec() (gas: 137284) LibLogTableBytesTest:testToBytesLogTableDecSmall() (gas: 141767) LibLogTableBytesTest:testToBytesLogTableDecSmallAlt() (gas: 18049) -LibParseDecimalFloatTest:testParseDecimalFloatEmpty() (gas: 4075) +LibParseDecimalFloatTest:testParseDecimalFloatEmpty() (gas: 4098) LibParseDecimalFloatTest:testParseDecimalFloatExponentRevert() (gas: 4115) LibParseDecimalFloatTest:testParseDecimalFloatExponentRevert2() (gas: 5291) LibParseDecimalFloatTest:testParseDecimalFloatExponentRevert3() (gas: 5375) @@ -193,17 +195,18 @@ LibParseDecimalFloatTest:testParseLiteralDecimalFloatDecimals() (gas: 378356) LibParseDecimalFloatTest:testParseLiteralDecimalFloatDotE() (gas: 4157) LibParseDecimalFloatTest:testParseLiteralDecimalFloatDotE0() (gas: 4135) LibParseDecimalFloatTest:testParseLiteralDecimalFloatDotRevert() (gas: 4113) -LibParseDecimalFloatTest:testParseLiteralDecimalFloatDotRevert2() (gas: 4135) -LibParseDecimalFloatTest:testParseLiteralDecimalFloatDotRevert3() (gas: 5076) +LibParseDecimalFloatTest:testParseLiteralDecimalFloatDotRevert2() (gas: 4113) +LibParseDecimalFloatTest:testParseLiteralDecimalFloatDotRevert3() (gas: 5120) LibParseDecimalFloatTest:testParseLiteralDecimalFloatEDot() (gas: 4159) LibParseDecimalFloatTest:testParseLiteralDecimalFloatExponentRevert5() (gas: 4145) LibParseDecimalFloatTest:testParseLiteralDecimalFloatExponentRevert6() (gas: 4092) LibParseDecimalFloatTest:testParseLiteralDecimalFloatExponents() (gas: 393083) -LibParseDecimalFloatTest:testParseLiteralDecimalFloatFuzz(uint256,uint8,bool) (runs: 5099, μ: 45464, ~: 36819) +LibParseDecimalFloatTest:testParseLiteralDecimalFloatFuzz(uint256,uint8,bool) (runs: 5099, μ: 45507, ~: 36636) LibParseDecimalFloatTest:testParseLiteralDecimalFloatLeadingZeros() (gas: 59042) -LibParseDecimalFloatTest:testParseLiteralDecimalFloatNegativeE() (gas: 6025) +LibParseDecimalFloatTest:testParseLiteralDecimalFloatNegativeE() (gas: 6048) LibParseDecimalFloatTest:testParseLiteralDecimalFloatNegativeFrac() (gas: 5095) LibParseDecimalFloatTest:testParseLiteralDecimalFloatPrecisionRevert0() (gas: 27442) -LibParseDecimalFloatTest:testParseLiteralDecimalFloatPrecisionRevert1() (gas: 27330) +LibParseDecimalFloatTest:testParseLiteralDecimalFloatPrecisionRevert1() (gas: 27353) LibParseDecimalFloatTest:testParseLiteralDecimalFloatSpecific() (gas: 22682) -LibParseDecimalFloatTest:testParseLiteralDecimalFloatUnrelated() (gas: 31978) \ No newline at end of file +LibParseDecimalFloatTest:testParseLiteralDecimalFloatUnrelated() (gas: 31978) +LibParseDecimalFloatTest:testParseMem(string) (runs: 5099, μ: 9205, ~: 9168) \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index e22fee06..54ad4b15 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "lib/rain.string"] path = lib/rain.string url = https://github.com/rainlanguage/rain.string +[submodule "lib/rain.math.fixedpoint"] + path = lib/rain.math.fixedpoint + url = https://github.com/rainlanguage/rain.math.fixedpoint diff --git a/lib/rain.math.fixedpoint b/lib/rain.math.fixedpoint new file mode 160000 index 00000000..6f8f7f8c --- /dev/null +++ b/lib/rain.math.fixedpoint @@ -0,0 +1 @@ +Subproject commit 6f8f7f8c79fd23b00aa5b9bda1171712e200023c diff --git a/slither.config.json b/slither.config.json index f857a90b..20da3cf9 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,4 +1,4 @@ { "detectors_to_exclude": "unused-imports,solc-version,dead-code,different-pragma-directives-are-used,assembly-usage,similar-names,naming-convention", - "filter_paths": "test/,lib/forge-std" + "filter_paths": "test/,lib/forge-std,lib/rain.math.fixedpoint,openzeppelin-contracts" } \ No newline at end of file diff --git a/src/lib/format/LibFormatDecimalFloat.sol b/src/lib/format/LibFormatDecimalFloat.sol new file mode 100644 index 00000000..a5623149 --- /dev/null +++ b/src/lib/format/LibFormatDecimalFloat.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 thedavidmeister +pragma solidity ^0.8.25; + +import {LibDecimalFloat, Float} from "../LibDecimalFloat.sol"; + +import {LibFixedPointDecimalFormat} from "rain.math.fixedpoint/lib/format/LibFixedPointDecimalFormat.sol"; + +library LibFormatDecimalFloat { + /// Format a decimal float as a string. + /// Currently is a thin wrapper around converting to a fixed point decimal + /// and then formatting that as a string. + /// In the future this may be extended to support a wider range of possible + /// values. + /// @param signedCoefficient The signed coefficient of the decimal float. + /// @param exponent The exponent of the decimal float. + /// @return The string representation of the decimal float. + function toDecimalString(int256 signedCoefficient, int256 exponent) internal pure returns (string memory) { + uint256 decimal18Value = LibDecimalFloat.toFixedDecimalLossless(signedCoefficient, exponent, 18); + return LibFixedPointDecimalFormat.fixedPointToDecimalString(decimal18Value); + } + + function toDecimalString(Float memory float) internal pure returns (string memory) { + return toDecimalString(float.signedCoefficient, float.exponent); + } +} diff --git a/src/lib/parse/LibParseDecimalFloat.sol b/src/lib/parse/LibParseDecimalFloat.sol index fe4b6469..1540d9fa 100644 --- a/src/lib/parse/LibParseDecimalFloat.sol +++ b/src/lib/parse/LibParseDecimalFloat.sol @@ -13,7 +13,7 @@ import { import {LibParseDecimal} from "rain.string/lib/parse/LibParseDecimal.sol"; import {MalformedExponentDigits, ParseDecimalPrecisionLoss, MalformedDecimalPoint} from "../../error/ErrParse.sol"; import {ParseDecimalOverflow, ParseEmptyDecimalString} from "rain.string/error/ErrParse.sol"; -import {LibDecimalFloat, PackedFloat} from "../LibDecimalFloat.sol"; +import {LibDecimalFloat, PackedFloat, Float} from "../LibDecimalFloat.sol"; import {LibDecimalFloatImplementation} from "../implementation/LibDecimalFloatImplementation.sol"; library LibParseDecimalFloat { @@ -140,4 +140,16 @@ library LibParseDecimalFloat { } } } + + function parseDecimalFloat(string memory str) internal pure returns (bytes4 errorSelector, Float memory float) { + uint256 start; + uint256 end; + assembly { + start := add(str, 0x20) + end := add(start, mload(str)) + } + uint256 cursor; + (errorSelector, cursor, float.signedCoefficient, float.exponent) = parseDecimalFloat(start, end); + (cursor); + } } diff --git a/test/src/lib/format/LibFormatDecimalFloat.t.sol b/test/src/lib/format/LibFormatDecimalFloat.t.sol new file mode 100644 index 00000000..2057bff6 --- /dev/null +++ b/test/src/lib/format/LibFormatDecimalFloat.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 thedavidmeister +pragma solidity =0.8.25; + +import {Test} from "forge-std/Test.sol"; +import {Float, LibDecimalFloat} from "src/lib/LibDecimalFloat.sol"; +import {LibFormatDecimalFloat} from "src/lib/format/LibFormatDecimalFloat.sol"; +import {LibParseDecimalFloat} from "src/lib/parse/LibParseDecimalFloat.sol"; + +/// @title LibFormatDecimalFloatTest +/// @notice Test contract for verifying the functionality of LibFormatDecimalFloat +/// @dev Tests both the stack and memory versions of formatting functions and round-trip conversions +contract LibFormatDecimalFloatTest is Test { + using LibDecimalFloat for Float; + + function toDecimalStringExternal(int256 signedCoefficient, int256 exponent) external pure returns (string memory) { + return LibFormatDecimalFloat.toDecimalString(signedCoefficient, exponent); + } + + function toString(Float memory float) external pure returns (string memory) { + return LibFormatDecimalFloat.toDecimalString(float); + } + + /// Check that the memory version matches the stack version. + function testFormatMem(Float memory float) external { + try this.toDecimalStringExternal(float.signedCoefficient, float.exponent) returns (string memory formatted) { + string memory actual = this.toString(float); + assertEq(formatted, actual, "Formatted value mismatch"); + } catch (bytes memory err) { + vm.expectRevert(err); + LibFormatDecimalFloat.toDecimalString(float); + } + } + + /// Test round tripping a value through parse and format. + function testRoundTrip(uint256 value) external pure { + // Dividing by 10 here keeps us clearly within the range of lossless + // conversions. + value = bound(value, 0, type(uint256).max / 10); + Float memory float = LibDecimalFloat.fromFixedDecimalLosslessMem(value, 18); + string memory formatted = LibFormatDecimalFloat.toDecimalString(float); + (bytes4 errorCode, Float memory parsed) = LibParseDecimalFloat.parseDecimalFloat(formatted); + assertEq(errorCode, 0, "Parse error"); + assertTrue(float.eq(parsed), "Round trip failed"); + } +} diff --git a/test/src/lib/parse/LibParseDecimalFloat.t.sol b/test/src/lib/parse/LibParseDecimalFloat.t.sol index 1f99b8a4..19e4e4a2 100644 --- a/test/src/lib/parse/LibParseDecimalFloat.t.sol +++ b/test/src/lib/parse/LibParseDecimalFloat.t.sol @@ -8,11 +8,45 @@ import {LibBytes, Pointer} from "rain.solmem/lib/LibBytes.sol"; import {Strings} from "openzeppelin-contracts/contracts/utils/Strings.sol"; import {ParseEmptyDecimalString} from "rain.string/error/ErrParse.sol"; import {MalformedExponentDigits, ParseDecimalPrecisionLoss, MalformedDecimalPoint} from "src/error/ErrParse.sol"; +import {Float} from "src/lib/LibDecimalFloat.sol"; contract LibParseDecimalFloatTest is Test { using LibBytes for bytes; using Strings for uint256; + function parseDecimalFloatExternal(string memory data) + external + pure + returns (bytes4 errorSelector, uint256 cursorAfter, int256 signedCoefficient, int256 exponent) + { + uint256 cursor = Pointer.unwrap(bytes(data).dataPointer()); + (errorSelector, cursorAfter, signedCoefficient, exponent) = + LibParseDecimalFloat.parseDecimalFloat(cursor, Pointer.unwrap(bytes(data).endDataPointer())); + } + + function parseDecimalFloatExternalMem(string memory data) + external + pure + returns (bytes4 errorSelector, Float memory float) + { + (errorSelector, float) = LibParseDecimalFloat.parseDecimalFloat(data); + } + + /// Check that the memory version matches the stack version. + function testParseMem(string memory data) external { + try this.parseDecimalFloatExternal(data) returns ( + bytes4 errorSelector, uint256 cursorAfter, int256 signedCoefficient, int256 exponent + ) { + (bytes4 errorSelectorMem, Float memory float) = this.parseDecimalFloatExternalMem(data); + assertEq(errorSelector, errorSelectorMem, "Error selector mismatch"); + assertEq(signedCoefficient, float.signedCoefficient, "Signed coefficient mismatch"); + assertEq(exponent, float.exponent, "Exponent mismatch"); + } catch (bytes memory err) { + vm.expectRevert(err); + this.parseDecimalFloatExternalMem(data); + } + } + function checkParseDecimalFloat( string memory data, int256 expectedSignedCoefficient,