From 1badad0a7a0d5c1c81fa2dbb486e2584f14a581f Mon Sep 17 00:00:00 2001 From: sdcb Date: Sun, 14 Dec 2025 17:32:36 +0800 Subject: [PATCH 1/5] Fix UInt128 to double conversion for values >= 2^104 Fixes #122203 --- .../System.Private.CoreLib/src/System/UInt128.cs | 2 +- .../tests/System.Runtime.Tests/System/UInt128Tests.cs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs index b76e5a04f5ce11..cc05f1768924db 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs @@ -275,7 +275,7 @@ public static explicit operator double(UInt128 value) // for the precision loss that double will have. As such, the lower value effectively drops the // lowest 24 bits and then or's them back to ensure rounding stays correct. - double lower = BitConverter.UInt64BitsToDouble(TwoPow76Bits | ((ulong)(value >> 12) >> 12) | (value._lower & 0xFFFFFF)) - TwoPow76; + double lower = BitConverter.UInt64BitsToDouble(TwoPow76Bits | ((ulong)(value >> 12) >> 12) | ((value._lower & 0xFFFFFF) != 0 ? 1UL : 0UL)) - TwoPow76; double upper = BitConverter.UInt64BitsToDouble(TwoPow128Bits | (ulong)(value >> 76)) - TwoPow128; return lower + upper; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs index a5d8716a2ecf2b..c871092f52b874 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs @@ -517,5 +517,13 @@ public static void BigMul(UInt128 a, UInt128 b, string result) UInt128 upper = UInt128.BigMul(a, b, out UInt128 lower); Assert.Equal(result, $"{upper:X32}{lower:X32}"); } + + [Fact] + public static void ExplicitConversionToDouble_LargeValue() + { + UInt128 value = UInt128.Parse("309485009821345068741558271"); + double d = (double)value; + Assert.Equal(3.094850098213451E+26, d); + } } } From 2dd043740b550d0e9316fc7410705f60744a2e1a Mon Sep 17 00:00:00 2001 From: sdcb Date: Sun, 14 Dec 2025 17:46:09 +0800 Subject: [PATCH 2/5] Apply suggestion: change shift threshold to 40 and add test case --- .../System.Private.CoreLib/src/System/UInt128.cs | 2 +- .../System.Runtime.Tests/System/UInt128Tests.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs index cc05f1768924db..c0e0cd721aa090 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs @@ -258,7 +258,7 @@ public static explicit operator double(UInt128 value) // For values between 0 and ulong.MaxValue, we just use the existing conversion return (double)(value._lower); } - else if ((value._upper >> 24) == 0) // value < (2^104) + else if ((value._upper >> 40) == 0) // value < (2^104) { // For values greater than ulong.MaxValue but less than 2^104 this takes advantage // that we can represent both "halves" of the uint128 within the 52-bit mantissa of diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs index c871092f52b874..cb066a1bbeb342 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs @@ -521,9 +521,22 @@ public static void BigMul(UInt128 a, UInt128 b, string result) [Fact] public static void ExplicitConversionToDouble_LargeValue() { + // Value: 309485009821345068741558271 (approx 2^88) + // This tests the path for values < 2^104 (after fix) or >= 2^88 (before fix) UInt128 value = UInt128.Parse("309485009821345068741558271"); double d = (double)value; Assert.Equal(3.094850098213451E+26, d); + + // Value >= 2^104 + // 2^104 = 20282409603651670423947251286016 + // We add 2^24 + 1 to ensure we test the sticky bit logic if it matters, + // or at least that the large value path doesn't crash or produce garbage. + // 2^104 + 2^24 + 1 + UInt128 value2 = (UInt128.One << 104) + (UInt128.One << 24) + 1; + double d2 = (double)value2; + // Expected: 2^104. 2^24 is far below ULP (2^52). + double expected2 = 20282409603651670423947251286016.0; + Assert.Equal(expected2, d2); } } } From 6ad9bfed9a75f56ccf976b5d7c2bb4b835f11e6f Mon Sep 17 00:00:00 2001 From: sdcb Date: Sun, 14 Dec 2025 23:32:17 +0800 Subject: [PATCH 3/5] 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 --- .../src/System/UInt128.cs | 2 +- .../System/Int128Tests.cs | 29 +++++++++++++++++++ .../System/UInt128Tests.cs | 6 ++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs index c0e0cd721aa090..a0dcc8aef1ee22 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs @@ -276,7 +276,7 @@ public static explicit operator double(UInt128 value) // lowest 24 bits and then or's them back to ensure rounding stays correct. double lower = BitConverter.UInt64BitsToDouble(TwoPow76Bits | ((ulong)(value >> 12) >> 12) | ((value._lower & 0xFFFFFF) != 0 ? 1UL : 0UL)) - TwoPow76; - double upper = BitConverter.UInt64BitsToDouble(TwoPow128Bits | (ulong)(value >> 76)) - TwoPow128; + double upper = BitConverter.UInt64BitsToDouble(TwoPow128Bits | (value._upper >> 12)) - TwoPow128; return lower + upper; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs index ab94d43f1355aa..5e5092d55d7b45 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs @@ -586,5 +586,34 @@ public static void BigMul(Int128 a, Int128 b, string result) Int128 upper = Int128.BigMul(a, b, out Int128 lower); Assert.Equal(result, $"{upper:X32}{lower:X32}"); } + + [Fact] + public static void ExplicitConversionToDouble_LargeValue() + { + // Value: 309485009821345068741558271 (approx 2^88) + Int128 value = Int128.Parse("309485009821345068741558271"); + double d = (double)value; + Assert.Equal(3.094850098213451E+26, d); + + // Negative Value + Int128 valueNeg = -value; + double dNeg = (double)valueNeg; + Assert.Equal(-3.094850098213451E+26, dNeg); + + // Value >= 2^104 + // The value is constructed as 2^104 + 2^24 + 1. + // This tests a value with a 1 at bit 104, a 1 at bit 24, and a 1 at bit 0. + // The lower bits (24 and 0) should contribute to the sticky bit calculation. + Int128 value2 = (Int128.One << 104) + (Int128.One << 24) + 1; + double d2 = (double)value2; + double expected2 = 20282409603651670423947251286016.0; + Assert.Equal(expected2, d2); + + // Negative Value >= 2^104 + Int128 value2Neg = -value2; + double d2Neg = (double)value2Neg; + Assert.Equal(-expected2, d2Neg); + } } } + diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs index cb066a1bbeb342..e14dc60b30b77c 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs @@ -529,9 +529,9 @@ public static void ExplicitConversionToDouble_LargeValue() // Value >= 2^104 // 2^104 = 20282409603651670423947251286016 - // We add 2^24 + 1 to ensure we test the sticky bit logic if it matters, - // or at least that the large value path doesn't crash or produce garbage. - // 2^104 + 2^24 + 1 + // The value is constructed as 2^104 + 2^24 + 1. + // This tests a value with a 1 at bit 104, a 1 at bit 24, and a 1 at bit 0. + // The lower bits (24 and 0) should contribute to the sticky bit calculation. UInt128 value2 = (UInt128.One << 104) + (UInt128.One << 24) + 1; double d2 = (double)value2; // Expected: 2^104. 2^24 is far below ULP (2^52). From 7357fab470cbc93795635506b2ea687143450806 Mon Sep 17 00:00:00 2001 From: sdcb Date: Tue, 16 Dec 2025 18:14:25 +0800 Subject: [PATCH 4/5] Make Int128/UInt128 test numbers more intuitive Use decimal literals instead of scientific notation and hex constructors instead of bit shifts for clearer test value representation. --- .../tests/System.Runtime.Tests/System/Int128Tests.cs | 6 +++--- .../tests/System.Runtime.Tests/System/UInt128Tests.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs index 5e5092d55d7b45..36b197524a0fec 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Int128Tests.cs @@ -593,18 +593,18 @@ public static void ExplicitConversionToDouble_LargeValue() // Value: 309485009821345068741558271 (approx 2^88) Int128 value = Int128.Parse("309485009821345068741558271"); double d = (double)value; - Assert.Equal(3.094850098213451E+26, d); + Assert.Equal(309485009821345068741558271.0, d); // Negative Value Int128 valueNeg = -value; double dNeg = (double)valueNeg; - Assert.Equal(-3.094850098213451E+26, dNeg); + Assert.Equal(-309485009821345068741558271.0, dNeg); // Value >= 2^104 // The value is constructed as 2^104 + 2^24 + 1. // This tests a value with a 1 at bit 104, a 1 at bit 24, and a 1 at bit 0. // The lower bits (24 and 0) should contribute to the sticky bit calculation. - Int128 value2 = (Int128.One << 104) + (Int128.One << 24) + 1; + Int128 value2 = new(0x0100_0000_0000, 0x0100_0001); double d2 = (double)value2; double expected2 = 20282409603651670423947251286016.0; Assert.Equal(expected2, d2); diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs index e14dc60b30b77c..634049dd2e667d 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/UInt128Tests.cs @@ -525,14 +525,14 @@ public static void ExplicitConversionToDouble_LargeValue() // This tests the path for values < 2^104 (after fix) or >= 2^88 (before fix) UInt128 value = UInt128.Parse("309485009821345068741558271"); double d = (double)value; - Assert.Equal(3.094850098213451E+26, d); + Assert.Equal(309485009821345068741558271.0, d); // Value >= 2^104 // 2^104 = 20282409603651670423947251286016 // The value is constructed as 2^104 + 2^24 + 1. // This tests a value with a 1 at bit 104, a 1 at bit 24, and a 1 at bit 0. // The lower bits (24 and 0) should contribute to the sticky bit calculation. - UInt128 value2 = (UInt128.One << 104) + (UInt128.One << 24) + 1; + UInt128 value2 = new(0x0100_0000_0000, 0x0100_0001); double d2 = (double)value2; // Expected: 2^104. 2^24 is far below ULP (2^52). double expected2 = 20282409603651670423947251286016.0; From 86a209b5247f0c5b25eeb1baa4e0b18c56e2fb2b Mon Sep 17 00:00:00 2001 From: sdcb Date: Sat, 20 Dec 2025 20:29:20 +0800 Subject: [PATCH 5/5] Fix UInt128 to double conversion for values in [2^88, 2^104) Change threshold check from (value._upper >> 24) to (value._upper >> 40) to correctly handle values up to 2^104 in the intermediate precision branch. --- src/libraries/System.Private.CoreLib/src/System/UInt128.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs index a0dcc8aef1ee22..20ad904bc423a2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt128.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt128.cs @@ -275,7 +275,7 @@ public static explicit operator double(UInt128 value) // for the precision loss that double will have. As such, the lower value effectively drops the // lowest 24 bits and then or's them back to ensure rounding stays correct. - double lower = BitConverter.UInt64BitsToDouble(TwoPow76Bits | ((ulong)(value >> 12) >> 12) | ((value._lower & 0xFFFFFF) != 0 ? 1UL : 0UL)) - TwoPow76; + double lower = BitConverter.UInt64BitsToDouble(TwoPow76Bits | ((ulong)(value >> 12) >> 12) | (value._lower & 0xFFFFFF)) - TwoPow76; double upper = BitConverter.UInt64BitsToDouble(TwoPow128Bits | (value._upper >> 12)) - TwoPow128; return lower + upper;