From 85fd3f2fd0e0a5018b22963ff6de9ee3a213c839 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 16 Sep 2025 14:14:02 +0200 Subject: [PATCH 1/4] pack 0 standardized --- src/lib/LibDecimalFloat.sol | 4 ++++ test/src/lib/LibDecimalFloat.floor.t.sol | 19 ++++++++++--------- test/src/lib/LibDecimalFloat.pack.t.sol | 7 +++++++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index 09dd2f31..f9bb755b 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -312,6 +312,10 @@ library LibDecimalFloat { signedCoefficient /= 10; ++exponent; } + } else { + if (signedCoefficient == 0) { + return (FLOAT_ZERO, true); + } } if (int32(exponent) != exponent) { diff --git a/test/src/lib/LibDecimalFloat.floor.t.sol b/test/src/lib/LibDecimalFloat.floor.t.sol index 8309d365..c0b54973 100644 --- a/test/src/lib/LibDecimalFloat.floor.t.sol +++ b/test/src/lib/LibDecimalFloat.floor.t.sol @@ -23,20 +23,21 @@ contract LibDecimalFloatFloorTest is Test { /// Every non negative exponent is identity for floor. function testFloorNonNegative(int224 x, int256 exponent) external pure { exponent = bound(exponent, 0, type(int32).max); - checkFloor(x, exponent, x, exponent); + checkFloor(x, exponent, x, x == 0 ? int256(0) : exponent); } /// If the exponent is less than -76 then the floor is 0. function testFloorLessThanMin(int224 x, int256 exponent) external pure { exponent = bound(exponent, type(int32).min, -77); - checkFloor(x, exponent, 0, exponent); + checkFloor(x, exponent, 0, 0); } /// For exponents [-76,-1] the floor is the / 1. function testFloorInRange(int224 x, int256 exponent) external pure { exponent = bound(exponent, -76, -1); int256 scale = int256(10 ** uint256(-exponent)); - checkFloor(x, exponent, (x / scale) * scale, exponent); + int256 y = (x / scale) * scale; + checkFloor(x, exponent, y, y == 0 ? int256(0) : exponent); } /// Examples @@ -50,9 +51,9 @@ contract LibDecimalFloatFloorTest is Test { checkFloor(123456789, -6, 123000000, -6); checkFloor(123456789, -7, 120000000, -7); checkFloor(123456789, -8, 100000000, -8); - checkFloor(123456789, -9, 0, -9); - checkFloor(123456789, -10, 0, -10); - checkFloor(123456789, -11, 0, -11); + checkFloor(123456789, -9, 0, 0); + checkFloor(123456789, -10, 0, 0); + checkFloor(123456789, -11, 0, 0); checkFloor(type(int224).max, 0, type(int224).max, 0); checkFloor(type(int224).min, 0, type(int224).min, 0); @@ -63,9 +64,9 @@ contract LibDecimalFloatFloorTest is Test { checkFloor(type(int224).max, -2, 13479973333575319897333507543509815336818572211270286240551805124600, -2); checkFloor(type(int224).max, -3, 13479973333575319897333507543509815336818572211270286240551805124000, -3); checkFloor(type(int224).max, -4, 13479973333575319897333507543509815336818572211270286240551805120000, -4); - checkFloor(type(int224).max, -77, 0, -77); - checkFloor(type(int224).max, -78, 0, -78); - checkFloor(type(int224).max, -76, 0, -76); + checkFloor(type(int224).max, -77, 0, 0); + checkFloor(type(int224).max, -78, 0, 0); + checkFloor(type(int224).max, -76, 0, 0); } function testFloorGasZero() external pure { diff --git a/test/src/lib/LibDecimalFloat.pack.t.sol b/test/src/lib/LibDecimalFloat.pack.t.sol index 065e9707..dcc8c81a 100644 --- a/test/src/lib/LibDecimalFloat.pack.t.sol +++ b/test/src/lib/LibDecimalFloat.pack.t.sol @@ -24,4 +24,11 @@ contract LibDecimalFloatPackTest is Test { assertEq(signedCoefficient, signedCoefficientOut, "coefficient"); assertEq(exponent, exponentOut, "exponent"); } + + /// Packing 0 is always lossless and returns standard zero float. + function testPackZero(int256 exponent) external pure { + (Float float, bool lossless) = LibDecimalFloat.packLossy(0, exponent); + assertTrue(lossless, "lossless"); + assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float"); + } } From 7aaa419220ff98a5fd1ca5cf0467378bcec14bc8 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 16 Sep 2025 14:47:11 +0200 Subject: [PATCH 2/4] zero pack lossy --- src/lib/LibDecimalFloat.sol | 9 ++++++++- test/src/lib/LibDecimalFloat.abs.t.sol | 2 +- test/src/lib/LibDecimalFloat.decimal.t.sol | 2 +- test/src/lib/LibDecimalFloat.decimalLossless.t.sol | 2 +- test/src/lib/LibDecimalFloat.frac.t.sol | 7 ++++--- test/src/lib/LibDecimalFloat.minus.t.sol | 2 +- test/src/lib/LibDecimalFloat.pack.t.sol | 10 +++++++++- 7 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/lib/LibDecimalFloat.sol b/src/lib/LibDecimalFloat.sol index f9bb755b..8f9ed14c 100644 --- a/src/lib/LibDecimalFloat.sol +++ b/src/lib/LibDecimalFloat.sol @@ -296,6 +296,8 @@ library LibDecimalFloat { /// exponent. function packLossy(int256 signedCoefficient, int256 exponent) internal pure returns (Float float, bool lossless) { unchecked { + int256 initialSignedCoefficient = signedCoefficient; + int256 initialExponent = exponent; lossless = int224(signedCoefficient) == signedCoefficient; // The reason that we can do unchecked exponent addition here is that @@ -319,7 +321,12 @@ library LibDecimalFloat { } if (int32(exponent) != exponent) { - revert ExponentOverflow(signedCoefficient, exponent); + // If the exponent is negative then this is a number too small + // to pack. We return zero but it is not a lossless conversion. + if (exponent < 0) { + return (FLOAT_ZERO, false); + } + revert ExponentOverflow(initialSignedCoefficient, initialExponent); } // Need a mask to zero out the bits that could be set to 1 if the diff --git a/test/src/lib/LibDecimalFloat.abs.t.sol b/test/src/lib/LibDecimalFloat.abs.t.sol index 2d5c5488..8bd2bd7e 100644 --- a/test/src/lib/LibDecimalFloat.abs.t.sol +++ b/test/src/lib/LibDecimalFloat.abs.t.sol @@ -16,7 +16,7 @@ contract LibDecimalFloatAbsTest is Test { Float result = float.abs(); (int256 resultSignedCoefficient, int256 resultExponent) = LibDecimalFloat.unpack(result); assertEq(resultSignedCoefficient, signedCoefficient); - assertEq(resultExponent, exponent); + assertEq(resultExponent, resultSignedCoefficient == 0 ? int256(0) : exponent); } /// Anything negative is negated. Except for the minimum value. diff --git a/test/src/lib/LibDecimalFloat.decimal.t.sol b/test/src/lib/LibDecimalFloat.decimal.t.sol index 7b126d8d..4714a0c5 100644 --- a/test/src/lib/LibDecimalFloat.decimal.t.sol +++ b/test/src/lib/LibDecimalFloat.decimal.t.sol @@ -31,7 +31,7 @@ contract LibDecimalFloatDecimalTest is Test { (Float float, bool floatLossless) = LibDecimalFloat.fromFixedDecimalLossyPacked(value, decimals); (int256 signedCoefficientFloat, int256 exponentFloat) = LibDecimalFloat.unpack(float); assertEq(signedCoefficientFloat, signedCoefficient, "signedCoefficient"); - assertEq(exponentFloat, exponent, "exponent"); + assertEq(exponentFloat, signedCoefficientFloat == 0 ? int256(0) : exponent, "exponent"); assertEq(floatLossless, lossless, "lossless"); } diff --git a/test/src/lib/LibDecimalFloat.decimalLossless.t.sol b/test/src/lib/LibDecimalFloat.decimalLossless.t.sol index 878610a3..b9710917 100644 --- a/test/src/lib/LibDecimalFloat.decimalLossless.t.sol +++ b/test/src/lib/LibDecimalFloat.decimalLossless.t.sol @@ -45,7 +45,7 @@ contract LibDecimalFloatDecimalLosslessTest is Test { (int256 signedCoefficient, int256 exponent) = LibDecimalFloat.fromFixedDecimalLossless(value, decimals); (int256 signedCoefficientPacked, int256 exponentPacked) = float.unpack(); assertEq(signedCoefficient, signedCoefficientPacked); - assertEq(exponent, exponentPacked); + assertEq(signedCoefficient == 0 ? int256(0) : exponent, exponentPacked); } function testToFixedDecimalLosslessPacked(Float float, uint8 decimals) external { diff --git a/test/src/lib/LibDecimalFloat.frac.t.sol b/test/src/lib/LibDecimalFloat.frac.t.sol index 5414f7ec..d438d888 100644 --- a/test/src/lib/LibDecimalFloat.frac.t.sol +++ b/test/src/lib/LibDecimalFloat.frac.t.sol @@ -23,20 +23,21 @@ contract LibDecimalFloatFracTest is Test { /// Every non negative exponent has no fractional component. function testFracNonNegative(int224 x, int256 exponent) external pure { exponent = bound(exponent, 0, type(int32).max); - checkFrac(x, exponent, 0, exponent); + checkFrac(x, exponent, 0, 0); } /// If the exponent is less than -76 then the fractional component is the /// same as the input. function testFracLessThanMin(int224 x, int256 exponent) external pure { exponent = bound(exponent, type(int32).min, -77); - checkFrac(x, exponent, x, exponent); + checkFrac(x, exponent, x, x == 0 ? int256(0) : exponent); } /// For exponents [-76,-1] the fractional component is the modulo of 1. function testFracInRange(int224 x, int256 exponent) external pure { exponent = bound(exponent, -76, -1); - checkFrac(x, exponent, x % int256(10 ** uint256(-exponent)), exponent); + int256 y = x % int256(10 ** uint256(-exponent)); + checkFrac(x, exponent, y, y == 0 ? int256(0) : exponent); } /// Examples diff --git a/test/src/lib/LibDecimalFloat.minus.t.sol b/test/src/lib/LibDecimalFloat.minus.t.sol index 233dcb68..cb8a5cd2 100644 --- a/test/src/lib/LibDecimalFloat.minus.t.sol +++ b/test/src/lib/LibDecimalFloat.minus.t.sol @@ -26,7 +26,7 @@ contract LibDecimalFloatMinusTest is Test { Float floatMinus = this.minusExternal(float); (int256 signedCoefficientMinus, int256 exponentMinus) = floatMinus.unpack(); assertEq(signedCoefficient, signedCoefficientMinus); - assertEq(exponent, exponentMinus); + assertEq(signedCoefficient == 0 ? int256(0) : exponent, exponentMinus); } catch (bytes memory err) { vm.expectRevert(err); this.minusExternal(float); diff --git a/test/src/lib/LibDecimalFloat.pack.t.sol b/test/src/lib/LibDecimalFloat.pack.t.sol index dcc8c81a..50d98bd6 100644 --- a/test/src/lib/LibDecimalFloat.pack.t.sol +++ b/test/src/lib/LibDecimalFloat.pack.t.sol @@ -22,7 +22,7 @@ contract LibDecimalFloatPackTest is Test { assertTrue(lossless, "lossless"); assertEq(signedCoefficient, signedCoefficientOut, "coefficient"); - assertEq(exponent, exponentOut, "exponent"); + assertEq(signedCoefficient == 0 ? int256(0) : exponent, exponentOut, "exponent"); } /// Packing 0 is always lossless and returns standard zero float. @@ -31,4 +31,12 @@ contract LibDecimalFloatPackTest is Test { assertTrue(lossless, "lossless"); assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float"); } + + /// Error when exponent larger than int32.max except for zero. + function testPackExponentOverflow(int256 signedCoefficient, int256 exponent) external { + exponent = bound(exponent, int256(type(int32).max) + 1, type(int256).max - 100); + vm.assume(signedCoefficient != 0); + vm.expectRevert(abi.encodeWithSelector(ExponentOverflow.selector, signedCoefficient, exponent)); + this.packLossyExternal(signedCoefficient, exponent); + } } From 9c67a63ac0df7099cbca8fdcc84158ab40925051 Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 16 Sep 2025 14:58:03 +0200 Subject: [PATCH 3/4] tests --- test/src/lib/LibDecimalFloat.pack.t.sol | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/src/lib/LibDecimalFloat.pack.t.sol b/test/src/lib/LibDecimalFloat.pack.t.sol index 50d98bd6..ba692428 100644 --- a/test/src/lib/LibDecimalFloat.pack.t.sol +++ b/test/src/lib/LibDecimalFloat.pack.t.sol @@ -34,9 +34,18 @@ contract LibDecimalFloatPackTest is Test { /// Error when exponent larger than int32.max except for zero. function testPackExponentOverflow(int256 signedCoefficient, int256 exponent) external { - exponent = bound(exponent, int256(type(int32).max) + 1, type(int256).max - 100); + exponent = bound(exponent, int256(type(int32).max) + 1, type(int256).max - 77); vm.assume(signedCoefficient != 0); vm.expectRevert(abi.encodeWithSelector(ExponentOverflow.selector, signedCoefficient, exponent)); this.packLossyExternal(signedCoefficient, exponent); } + + /// Lossy zero when exponent is negative below type(int32).min except for zero. + function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external view { + exponent = bound(exponent, type(int256).min, int256(type(int32).min) - 77); + vm.assume(signedCoefficient != 0); + (Float float, bool lossless) = this.packLossyExternal(signedCoefficient, exponent); + assertFalse(lossless, "lossless"); + assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float"); + } } From 06ba31ea9917060bf77ba618000d3cfd53d35add Mon Sep 17 00:00:00 2001 From: thedavidmeister Date: Tue, 16 Sep 2025 15:09:13 +0200 Subject: [PATCH 4/4] fix rust tests --- crates/float/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/float/src/lib.rs b/crates/float/src/lib.rs index 5830f8a1..2f05442f 100644 --- a/crates/float/src/lib.rs +++ b/crates/float/src/lib.rs @@ -1543,11 +1543,8 @@ mod tests { let near_min_exp = Float::parse("1e-2147483646".to_string()).unwrap(); let one_e_neg_three = Float::parse("1e-3".to_string()).unwrap(); - let err = (near_min_exp * one_e_neg_three).unwrap_err(); - assert!(matches!( - err, - FloatError::DecimalFloat(DecimalFloatErrors::ExponentOverflow(_)) - )); + let float = (near_min_exp * one_e_neg_three).unwrap(); + assert!(float.is_zero().unwrap()); } #[test]