Skip to content

Commit c808380

Browse files
committed
Fix (U)Int128 to double conversion precision loss
- Correctly calculate the sticky bit when converting large UInt128 values (>= 2^104) to double to prevent precision loss. - Optimize the upper bits extraction by accessing _upper directly instead of shifting. - Add regression tests for Int128 to ensure it is also fixed. - Update test comments to better explain the test cases. Fixes #122203
1 parent f9779df commit c808380

File tree

3 files changed

+33
-4
lines changed

3 files changed

+33
-4
lines changed

src/libraries/System.Private.CoreLib/src/System/UInt128.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ public static explicit operator double(UInt128 value)
276276
// lowest 24 bits and then or's them back to ensure rounding stays correct.
277277

278278
double lower = BitConverter.UInt64BitsToDouble(TwoPow76Bits | ((ulong)(value >> 12) >> 12) | ((value._lower & 0xFFFFFF) != 0 ? 1UL : 0UL)) - TwoPow76;
279-
double upper = BitConverter.UInt64BitsToDouble(TwoPow128Bits | (ulong)(value >> 76)) - TwoPow128;
279+
double upper = BitConverter.UInt64BitsToDouble(TwoPow128Bits | (value._upper >> 12)) - TwoPow128;
280280

281281
return lower + upper;
282282
}

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,5 +586,34 @@ public static void BigMul(Int128 a, Int128 b, string result)
586586
Int128 upper = Int128.BigMul(a, b, out Int128 lower);
587587
Assert.Equal(result, $"{upper:X32}{lower:X32}");
588588
}
589+
590+
[Fact]
591+
public static void ExplicitConversionToDouble_LargeValue()
592+
{
593+
// Value: 309485009821345068741558271 (approx 2^88)
594+
Int128 value = Int128.Parse("309485009821345068741558271");
595+
double d = (double)value;
596+
Assert.Equal(3.094850098213451E+26, d);
597+
598+
// Negative Value
599+
Int128 valueNeg = -value;
600+
double dNeg = (double)valueNeg;
601+
Assert.Equal(-3.094850098213451E+26, dNeg);
602+
603+
// Value >= 2^104
604+
// The value is constructed as 2^104 + 2^24 + 1.
605+
// This tests a value with a 1 at bit 104, a 1 at bit 24, and a 1 at bit 0.
606+
// The lower bits (24 and 0) should contribute to the sticky bit calculation.
607+
Int128 value2 = (Int128.One << 104) + (Int128.One << 24) + 1;
608+
double d2 = (double)value2;
609+
double expected2 = 20282409603651670423947251286016.0;
610+
Assert.Equal(expected2, d2);
611+
612+
// Negative Value >= 2^104
613+
Int128 value2Neg = -value2;
614+
double d2Neg = (double)value2Neg;
615+
Assert.Equal(-expected2, d2Neg);
616+
}
589617
}
590618
}
619+

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -529,9 +529,9 @@ public static void ExplicitConversionToDouble_LargeValue()
529529

530530
// Value >= 2^104
531531
// 2^104 = 20282409603651670423947251286016
532-
// We add 2^24 + 1 to ensure we test the sticky bit logic if it matters,
533-
// or at least that the large value path doesn't crash or produce garbage.
534-
// 2^104 + 2^24 + 1
532+
// The value is constructed as 2^104 + 2^24 + 1.
533+
// This tests a value with a 1 at bit 104, a 1 at bit 24, and a 1 at bit 0.
534+
// The lower bits (24 and 0) should contribute to the sticky bit calculation.
535535
UInt128 value2 = (UInt128.One << 104) + (UInt128.One << 24) + 1;
536536
double d2 = (double)value2;
537537
// Expected: 2^104. 2^24 is far below ULP (2^52).

0 commit comments

Comments
 (0)