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
7 changes: 2 additions & 5 deletions crates/float/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
13 changes: 12 additions & 1 deletion src/lib/LibDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
thedavidmeister marked this conversation as resolved.

// The reason that we can do unchecked exponent addition here is that
Expand All @@ -312,10 +314,19 @@ library LibDecimalFloat {
signedCoefficient /= 10;
++exponent;
}
} else {
if (signedCoefficient == 0) {
return (FLOAT_ZERO, true);
}
}

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
Expand Down
2 changes: 1 addition & 1 deletion test/src/lib/LibDecimalFloat.abs.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion test/src/lib/LibDecimalFloat.decimal.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down
2 changes: 1 addition & 1 deletion test/src/lib/LibDecimalFloat.decimalLossless.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
19 changes: 10 additions & 9 deletions test/src/lib/LibDecimalFloat.floor.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand All @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions test/src/lib/LibDecimalFloat.frac.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/src/lib/LibDecimalFloat.minus.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
26 changes: 25 additions & 1 deletion test/src/lib/LibDecimalFloat.pack.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,30 @@ 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.
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");
}
Comment thread
thedavidmeister marked this conversation as resolved.

/// 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 - 77);
vm.assume(signedCoefficient != 0);
vm.expectRevert(abi.encodeWithSelector(ExponentOverflow.selector, signedCoefficient, exponent));
this.packLossyExternal(signedCoefficient, exponent);
}
Comment thread
thedavidmeister marked this conversation as resolved.

/// 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");
Comment on lines +44 to +49
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick

Tighten the underflow test: make it pure and target the threshold precisely.

  • Mark the test pure (no state read/write).
  • Bound signedCoefficient to ≥1 to avoid discards.
  • Pin exponent to a value ≤ int32.min − 77 to deterministically exercise the lossy‑zero path.

Apply this diff:

-    function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external view {
-        exponent = bound(exponent, type(int256).min, int256(type(int32).min) - 77);
-        vm.assume(signedCoefficient != 0);
+    function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external pure {
+        signedCoefficient = bound(signedCoefficient, 1, type(int256).max);
+        exponent = bound(exponent, int256(type(int32).min) - 77, int256(type(int32).min) - 77);
         (Float float, bool lossless) = this.packLossyExternal(signedCoefficient, exponent);
         assertFalse(lossless, "lossless");
         assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float");
     }

Consider adding a sibling test for the “within 1..76” underflow window to assert non‑zero, lossy pack clamped at int32.min.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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");
function testPackNegativeExponentLossyZero(int256 signedCoefficient, int256 exponent) external pure {
signedCoefficient = bound(signedCoefficient, 1, type(int256).max);
exponent = bound(exponent, int256(type(int32).min) - 77, int256(type(int32).min) - 77);
(Float float, bool lossless) = this.packLossyExternal(signedCoefficient, exponent);
assertFalse(lossless, "lossless");
assertEq(Float.unwrap(float), Float.unwrap(LibDecimalFloat.FLOAT_ZERO), "float");
}

}
}