Add debugger display for huge BigInteger (#75796)
authorHuo Yaoyuan <huoyaoyuan@hotmail.com>
Sun, 8 Jan 2023 03:46:57 +0000 (11:46 +0800)
committerGitHub <noreply@github.com>
Sun, 8 Jan 2023 03:46:57 +0000 (22:46 -0500)
* Add debugger display for huge BigInteger

* Apply suggestions from code review

Co-authored-by: tfenise <tfenise@live.com>
* Adjust for overflow and rounding

* Fix and add test

* Apply suggestions from code review

Co-authored-by: tfenise <tfenise@live.com>
* Skip test on browser

* Adjust overflow handling and comment

* Apply suggestions from code review

Co-authored-by: Dan Moseley <danmose@microsoft.com>
* Revert "Adjust overflow handling and comment"

This reverts commit ceac7654aeb7ce4b2b91f6d84cf891c4bcdce755.

* Update ToString culture

Co-authored-by: tfenise <tfenise@live.com>
* Apply suggestions from code review

Co-authored-by: Tanner Gooding <tagoo@outlook.com>
* Update for constant and shortcut

* Use invariant culture in test

* Use log10(2) as a constant

Co-authored-by: Drew Kersnar <18474647+dakersnar@users.noreply.github.com>
* Update length thresold to 4*uint

* Update bigger test cases

* Update test value

Co-authored-by: tfenise <tfenise@live.com>
Co-authored-by: Dan Moseley <danmose@microsoft.com>
Co-authored-by: Tanner Gooding <tagoo@outlook.com>
Co-authored-by: Drew Kersnar <18474647+dakersnar@users.noreply.github.com>
src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs
src/libraries/System.Runtime.Numerics/tests/BigInteger/DebuggerDisplayTests.cs [new file with mode: 0644]
src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj

index 82a1a5c..b8addb9 100644 (file)
@@ -13,6 +13,7 @@ namespace System.Numerics
 {
     [Serializable]
     [TypeForwardedFrom("System.Numerics, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089")]
+    [DebuggerDisplay("{DebuggerDisplay,nq}")]
     public readonly struct BigInteger
         : ISpanFormattable,
           IComparable,
@@ -1588,6 +1589,61 @@ namespace System.Numerics
             return BigNumber.FormatBigInteger(this, format, NumberFormatInfo.GetInstance(provider));
         }
 
+        private string DebuggerDisplay
+        {
+            get
+            {
+                // For very big numbers, ToString can be too long or even timeout for Visual Studio to display
+                // Display a fast estimated value instead
+
+                // Use ToString for small values
+
+                if ((_bits is null) || (_bits.Length <= 4))
+                {
+                    return ToString();
+                }
+
+                // Estimate the value x as `L * 2^n`, while L is the value of high bits, and n is the length of low bits
+                // Represent L as `k * 10^i`, then `x = L * 2^n = k * 10^(i + (n * log10(2)))`
+                // Let `m = n * log10(2)`, the final result would be `x = (k * 10^(m - [m])) * 10^(i+[m])`
+
+                const double log10Of2 = 0.3010299956639812; // Log10(2)
+                ulong highBits = ((ulong)_bits[^1] << kcbitUint) + _bits[^2];
+                double lowBitsCount32 = _bits.Length - 2; // if Length > int.MaxValue/32, counting in bits can cause overflow
+                double exponentLow = lowBitsCount32 * kcbitUint * log10Of2;
+
+                // Max possible length of _bits is int.MaxValue of bytes,
+                // thus max possible value of BigInteger is 2^(8*Array.MaxLength)-1 which is larger than 10^(2^33)
+                // Use long to avoid potential overflow
+                long exponent = (long)exponentLow;
+                double significand = (double)highBits * Math.Pow(10, exponentLow - exponent);
+
+                // scale significand to [1, 10)
+                double log10 = Math.Log10(significand);
+                if (log10 >= 1)
+                {
+                    exponent += (long)log10;
+                    significand /= Math.Pow(10, Math.Floor(log10));
+                }
+
+                // The digits can be incorrect because of floating point errors and estimation in Log and Exp
+                // Keep some digits in the significand. 8 is arbitrarily chosen, about half of the precision of double
+                significand = Math.Round(significand, 8);
+
+                if (significand >= 10.0)
+                {
+                    // 9.9999999999999 can be rounded to 10, make the display to be more natural
+                    significand /= 10.0;
+                    exponent++;
+                }
+
+                string signStr = _sign < 0 ? NumberFormatInfo.CurrentInfo.NegativeSign : "";
+
+                // Use about a half of the precision of double
+                return $"{signStr}{significand:F8}e+{exponent}";
+            }
+        }
+
         public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.NumericFormat)] ReadOnlySpan<char> format = default, IFormatProvider? provider = null)
         {
             return BigNumber.TryFormatBigInteger(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten);
diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/DebuggerDisplayTests.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/DebuggerDisplayTests.cs
new file mode 100644 (file)
index 0000000..d74da86
--- /dev/null
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Globalization;
+using System.Tests;
+using Xunit;
+
+namespace System.Numerics.Tests
+{
+    public class DebuggerDisplayTests
+    {
+        [Theory]
+        [InlineData(new uint[] { 0, 0, 1 }, "18446744073709551616")]
+        [InlineData(new uint[] { 0, 0, 0, 0, 1 }, "3.40282367e+38")]
+        [InlineData(new uint[] { 0, 0x12345678, 0, 0xCC00CC00, 0x80808080 }, "7.33616508e+47")]
+        [SkipOnPlatform(TestPlatforms.Browser, "DebuggerDisplayAttribute is stripped on wasm")]
+        public void TestDebuggerDisplay(uint[] bits, string displayString)
+        {
+            using (new ThreadCultureChange(CultureInfo.InvariantCulture))
+            {
+                BigInteger positiveValue = new BigInteger(1, bits);
+                Assert.Equal(displayString, DebuggerAttributes.ValidateDebuggerDisplayReferences(positiveValue));
+
+                BigInteger negativeValue = new BigInteger(-1, bits);
+                Assert.Equal("-" + displayString, DebuggerAttributes.ValidateDebuggerDisplayReferences(negativeValue));
+            }
+        }
+    }
+}
index 5417865..0716f70 100644 (file)
@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
     <IncludeRemoteExecutor>true</IncludeRemoteExecutor>
@@ -16,6 +16,7 @@
     <Compile Include="BigInteger\cast_to.cs" />
     <Compile Include="BigInteger\Comparison.cs" />
     <Compile Include="BigInteger\ctor.cs" />
+    <Compile Include="BigInteger\DebuggerDisplayTests.cs" />
     <Compile Include="BigInteger\divide.cs" />
     <Compile Include="BigInteger\divrem.cs" />
     <Compile Include="BigInteger\Driver.cs" />
@@ -56,6 +57,7 @@
   </ItemGroup>
   <ItemGroup>
     <Compile Include="$(CommonTestPath)System\GenericMathHelpers.cs" Link="Common\System\GenericMathHelpers.cs" />
+    <Compile Include="$(CommonTestPath)System\Diagnostics\DebuggerAttributes.cs" Link="Common\System\Diagnostics\DebuggerAttributes.cs" />
     <Compile Include="BigIntegerTests.GenericMath.cs" />
     <Compile Include="ComplexTests.GenericMath.cs" />
   </ItemGroup>