From 876617a496f88d5e1de23c990aa880cb3a4e5929 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:50:09 +0000 Subject: [PATCH 01/20] Fix BigInteger.LeadingZeroCount to restore 32-bit behavior and add tests Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ac91600f-e8a6-44ef-adcb-9f02df235fb0 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 29 ++++- .../tests/BigInteger/LeadingZeroCountTests.cs | 110 ++++++++++++++++++ .../tests/BigIntegerTests.GenericMath.cs | 4 +- .../System.Runtime.Numerics.Tests.csproj | 1 + 4 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index aa6389fcf94c8f..236434ed4f048e 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3153,15 +3153,34 @@ public static BigInteger LeadingZeroCount(BigInteger value) { if (value._bits is null) { - return nint.LeadingZeroCount(value._sign); + // For small values stored in _sign, use 32-bit counting to match the + // behavior when _bits was uint[] (where each limb was always 32-bit). + return uint.LeadingZeroCount((uint)value._sign); } - // When the value is positive, we just need to get the lzcnt of the most significant bits. // When negative, two's complement has infinite sign-extension of 1-bits, so LZC is always 0. + if (value._sign < 0) + { + return 0; + } + + // When positive, count leading zeros in the most significant 32-bit word of the value. + // On 64-bit systems, each nuint limb holds 64 bits, so we extract the most significant + // 32-bit half. This preserves the behavior from when _bits was uint[] (32-bit limbs). + nuint msLimb = value._bits[^1]; + uint msWord; + + if (nuint.Size > sizeof(uint)) + { + uint high = (uint)(msLimb >> 32); + msWord = (high != 0) ? high : (uint)msLimb; + } + else + { + msWord = (uint)msLimb; + } - return (value._sign >= 0) - ? BitOperations.LeadingZeroCount(value._bits[^1]) - : 0; + return uint.LeadingZeroCount(msWord); } /// diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs new file mode 100644 index 00000000000000..f1f4784bba3719 --- /dev/null +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Numerics.Tests +{ + public class LeadingZeroCountTests + { + // LeadingZeroCount is defined to count leading zeros in the most significant 32-bit word of + // the value's magnitude (same as when BigInteger's internal _bits array used uint[] limbs). + // Negative values have infinite sign-extension of 1-bits, so their LZC is always 0. + + // Values stored in _sign (magnitude <= int.MaxValue): + // LZC is computed on the 32-bit representation of _sign. + + // Values stored in _bits (magnitude > int.MaxValue): + // LZC is the leading zero count of the most significant 32-bit word of the value. + // On 64-bit systems a limb holds 64 bits; we extract the most significant 32-bit half. + + [Theory] + [InlineData(0, 32)] // Zero: 32 leading zeros + [InlineData(1, 31)] // 0x00000001 + [InlineData(2, 30)] // 0x00000002 + [InlineData(0x7FFFFFFF, 1)] // int.MaxValue: 0x7FFFFFFF, 1 leading zero + [InlineData(-1, 0)] // Negative: always 0 + [InlineData(-2, 0)] // Negative + [InlineData(int.MinValue + 1, 0)] // Most negative value stored in _sign (-int.MaxValue) + public static void SmallValues(int value, int expected) + { + Assert.Equal((BigInteger)expected, BigInteger.LeadingZeroCount(new BigInteger(value))); + } + + [Theory] + // Boundary at 2^31 (int.MaxValue + 1 = 0x80000000): first value that goes into _bits. + // Leading "0" prefix keeps the high bit clear, making BigInteger interpret as positive. + [InlineData("080000000", 0)] // 2^31: MSW = 0x80000000 + [InlineData("0FFFFFFFF", 0)] // uint.MaxValue: MSW = 0xFFFFFFFF + [InlineData("0100000000", 31)] // 2^32: MSW = 0x00000001 + [InlineData("0100000001", 31)] // 2^32 + 1: MSW = 0x00000001 + [InlineData("07FFFFFFF00000000", 1)] // MSW of most-significant 32-bit word = 0x7FFFFFFF + [InlineData("07FFFFFFFFFFFFFFF", 1)] // long.MaxValue: MSW = 0x7FFFFFFF + [InlineData("08000000000000000", 0)] // 2^63 (= long.MaxValue + 1): MSW = 0x80000000 + [InlineData("0FFFFFFFFFFFFFFFF", 0)] // ulong.MaxValue: MSW = 0xFFFFFFFF + [InlineData("010000000000000000", 31)] // 2^64: MSW = 0x00000001 + [InlineData("080000000000000000000000000000000", 0)] // 2^127: MSW = 0x80000000 + [InlineData("0100000000000000000000000000000000", 31)] // 2^128: MSW = 0x00000001 + public static void LargePositiveValues(string hexValue, int expected) + { + BigInteger value = BigInteger.Parse(hexValue, Globalization.NumberStyles.HexNumber); + Assert.Equal((BigInteger)expected, BigInteger.LeadingZeroCount(value)); + } + + [Theory] + // Negative values always have LZC = 0 (infinite sign-extension of 1-bits). + // Construct large negative values as negations of known positive magnitudes. + [InlineData("080000000")] // -(2^31): magnitude stored in _bits + [InlineData("0FFFFFFFF")] // -(uint.MaxValue): magnitude stored in _bits + [InlineData("0100000000")] // -(2^32): magnitude stored in _bits + [InlineData("07FFFFFFFFFFFFFFF")] // -(long.MaxValue): magnitude stored in _bits + [InlineData("08000000000000000")] // -(2^63): magnitude stored in _bits + public static void LargeNegativeValues(string hexMagnitude) + { + // Parse the magnitude as a positive hex value (leading zero keeps high bit clear), + // then negate it so the result is negative and stored in _bits. + BigInteger magnitude = BigInteger.Parse(hexMagnitude, Globalization.NumberStyles.HexNumber); + BigInteger value = -magnitude; + Assert.True(value < 0); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(value)); + } + + [Fact] + public static void IntMinValue() + { + // int.MinValue (-2^31) is stored in _bits (excluded from the _sign-only range), + // and it's negative so LZC = 0. + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(new BigInteger(int.MinValue))); + } + + [Fact] + public static void LzcIsAlwaysNonNegative() + { + // Regardless of how big the value is, LeadingZeroCount is always >= 0. + BigInteger hugePositive = BigInteger.Pow(2, 1000); + BigInteger result = BigInteger.LeadingZeroCount(hugePositive); + Assert.True(result >= 0); + } + + [Fact] + public static void PlatformIndependence() + { + // Results must be the same on 32-bit and 64-bit platforms. + // For Zero: always 32, not nint.Size * 8. + Assert.Equal((BigInteger)32, BigInteger.LeadingZeroCount(BigInteger.Zero)); + + // For One: always 31, not nint.Size * 8 - 1. + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.One)); + + // For values crossing the 32-bit/64-bit limb boundary, results must be consistent. + // 2^32: MSW in 32-bit view = 0x00000001, so LZC = 31. + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 32))); + + // 2^63: MSW in 32-bit view = 0x80000000, so LZC = 0. + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 63))); + + // 2^64: MSW in 32-bit view = 0x00000001, so LZC = 31. + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 64))); + } + } +} diff --git a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs index 721ac8bc3fe9d7..4f3856d5c9024f 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs @@ -209,8 +209,8 @@ public static void DivRemTest() [Fact] public static void LeadingZeroCountTest() { - Assert.Equal((BigInteger)(nint.Size * 8), BinaryIntegerHelper.LeadingZeroCount(Zero)); - Assert.Equal((BigInteger)(nint.Size * 8 - 1), BinaryIntegerHelper.LeadingZeroCount(One)); + Assert.Equal((BigInteger)32, BinaryIntegerHelper.LeadingZeroCount(Zero)); + Assert.Equal((BigInteger)31, BinaryIntegerHelper.LeadingZeroCount(One)); Assert.Equal((BigInteger)1, BinaryIntegerHelper.LeadingZeroCount(Int64MaxValue)); Assert.Equal((BigInteger)0, BinaryIntegerHelper.LeadingZeroCount(Int64MinValue)); diff --git a/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj b/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj index 540676f2d3e1af..defe6b20b3ec8a 100644 --- a/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj +++ b/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj @@ -21,6 +21,7 @@ + From bef3d9b2823d3479787e4459e4cdf82efc436d3a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:36:11 +0000 Subject: [PATCH 02/20] Address review feedback: use Environment.Is64BitProcess, merge tests into GenericMath Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/82e43e2a-a6a2-4c40-ad57-ffaa52c2cc18 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 2 +- .../tests/BigInteger/LeadingZeroCountTests.cs | 110 ------------------ .../tests/BigIntegerTests.GenericMath.cs | 41 +++++++ .../System.Runtime.Numerics.Tests.csproj | 1 - 4 files changed, 42 insertions(+), 112 deletions(-) delete mode 100644 src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 236434ed4f048e..eb178cf4eab79d 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3170,7 +3170,7 @@ public static BigInteger LeadingZeroCount(BigInteger value) nuint msLimb = value._bits[^1]; uint msWord; - if (nuint.Size > sizeof(uint)) + if (Environment.Is64BitProcess) { uint high = (uint)(msLimb >> 32); msWord = (high != 0) ? high : (uint)msLimb; diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs deleted file mode 100644 index f1f4784bba3719..00000000000000 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/LeadingZeroCountTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace System.Numerics.Tests -{ - public class LeadingZeroCountTests - { - // LeadingZeroCount is defined to count leading zeros in the most significant 32-bit word of - // the value's magnitude (same as when BigInteger's internal _bits array used uint[] limbs). - // Negative values have infinite sign-extension of 1-bits, so their LZC is always 0. - - // Values stored in _sign (magnitude <= int.MaxValue): - // LZC is computed on the 32-bit representation of _sign. - - // Values stored in _bits (magnitude > int.MaxValue): - // LZC is the leading zero count of the most significant 32-bit word of the value. - // On 64-bit systems a limb holds 64 bits; we extract the most significant 32-bit half. - - [Theory] - [InlineData(0, 32)] // Zero: 32 leading zeros - [InlineData(1, 31)] // 0x00000001 - [InlineData(2, 30)] // 0x00000002 - [InlineData(0x7FFFFFFF, 1)] // int.MaxValue: 0x7FFFFFFF, 1 leading zero - [InlineData(-1, 0)] // Negative: always 0 - [InlineData(-2, 0)] // Negative - [InlineData(int.MinValue + 1, 0)] // Most negative value stored in _sign (-int.MaxValue) - public static void SmallValues(int value, int expected) - { - Assert.Equal((BigInteger)expected, BigInteger.LeadingZeroCount(new BigInteger(value))); - } - - [Theory] - // Boundary at 2^31 (int.MaxValue + 1 = 0x80000000): first value that goes into _bits. - // Leading "0" prefix keeps the high bit clear, making BigInteger interpret as positive. - [InlineData("080000000", 0)] // 2^31: MSW = 0x80000000 - [InlineData("0FFFFFFFF", 0)] // uint.MaxValue: MSW = 0xFFFFFFFF - [InlineData("0100000000", 31)] // 2^32: MSW = 0x00000001 - [InlineData("0100000001", 31)] // 2^32 + 1: MSW = 0x00000001 - [InlineData("07FFFFFFF00000000", 1)] // MSW of most-significant 32-bit word = 0x7FFFFFFF - [InlineData("07FFFFFFFFFFFFFFF", 1)] // long.MaxValue: MSW = 0x7FFFFFFF - [InlineData("08000000000000000", 0)] // 2^63 (= long.MaxValue + 1): MSW = 0x80000000 - [InlineData("0FFFFFFFFFFFFFFFF", 0)] // ulong.MaxValue: MSW = 0xFFFFFFFF - [InlineData("010000000000000000", 31)] // 2^64: MSW = 0x00000001 - [InlineData("080000000000000000000000000000000", 0)] // 2^127: MSW = 0x80000000 - [InlineData("0100000000000000000000000000000000", 31)] // 2^128: MSW = 0x00000001 - public static void LargePositiveValues(string hexValue, int expected) - { - BigInteger value = BigInteger.Parse(hexValue, Globalization.NumberStyles.HexNumber); - Assert.Equal((BigInteger)expected, BigInteger.LeadingZeroCount(value)); - } - - [Theory] - // Negative values always have LZC = 0 (infinite sign-extension of 1-bits). - // Construct large negative values as negations of known positive magnitudes. - [InlineData("080000000")] // -(2^31): magnitude stored in _bits - [InlineData("0FFFFFFFF")] // -(uint.MaxValue): magnitude stored in _bits - [InlineData("0100000000")] // -(2^32): magnitude stored in _bits - [InlineData("07FFFFFFFFFFFFFFF")] // -(long.MaxValue): magnitude stored in _bits - [InlineData("08000000000000000")] // -(2^63): magnitude stored in _bits - public static void LargeNegativeValues(string hexMagnitude) - { - // Parse the magnitude as a positive hex value (leading zero keeps high bit clear), - // then negate it so the result is negative and stored in _bits. - BigInteger magnitude = BigInteger.Parse(hexMagnitude, Globalization.NumberStyles.HexNumber); - BigInteger value = -magnitude; - Assert.True(value < 0); - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(value)); - } - - [Fact] - public static void IntMinValue() - { - // int.MinValue (-2^31) is stored in _bits (excluded from the _sign-only range), - // and it's negative so LZC = 0. - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(new BigInteger(int.MinValue))); - } - - [Fact] - public static void LzcIsAlwaysNonNegative() - { - // Regardless of how big the value is, LeadingZeroCount is always >= 0. - BigInteger hugePositive = BigInteger.Pow(2, 1000); - BigInteger result = BigInteger.LeadingZeroCount(hugePositive); - Assert.True(result >= 0); - } - - [Fact] - public static void PlatformIndependence() - { - // Results must be the same on 32-bit and 64-bit platforms. - // For Zero: always 32, not nint.Size * 8. - Assert.Equal((BigInteger)32, BigInteger.LeadingZeroCount(BigInteger.Zero)); - - // For One: always 31, not nint.Size * 8 - 1. - Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.One)); - - // For values crossing the 32-bit/64-bit limb boundary, results must be consistent. - // 2^32: MSW in 32-bit view = 0x00000001, so LZC = 31. - Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 32))); - - // 2^63: MSW in 32-bit view = 0x80000000, so LZC = 0. - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 63))); - - // 2^64: MSW in 32-bit view = 0x00000001, so LZC = 31. - Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 64))); - } - } -} diff --git a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs index 4f3856d5c9024f..ce48f8a263dce9 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs @@ -218,6 +218,47 @@ public static void LeadingZeroCountTest() Assert.Equal((BigInteger)0, BinaryIntegerHelper.LeadingZeroCount(Int64MaxValuePlusOne)); Assert.Equal((BigInteger)0, BinaryIntegerHelper.LeadingZeroCount(UInt64MaxValue)); + + // Small values stored in _sign: LZC is based on 32-bit width. + Assert.Equal((BigInteger)32, BigInteger.LeadingZeroCount(new BigInteger(0))); + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(new BigInteger(1))); + Assert.Equal((BigInteger)30, BigInteger.LeadingZeroCount(new BigInteger(2))); + Assert.Equal((BigInteger)1, BigInteger.LeadingZeroCount(new BigInteger(int.MaxValue))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(new BigInteger(-1))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(new BigInteger(-2))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(new BigInteger(int.MinValue))); + + // Large positive values: LZC is the leading zero count of the most significant 32-bit word. + // 2^31 (= int.MaxValue+1): MSW = 0x80000000, LZC = 0 + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + // uint.MaxValue (0xFFFFFFFF): MSW = 0xFFFFFFFF, LZC = 0 + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("0FFFFFFFF", Globalization.NumberStyles.HexNumber))); + // 2^32 (= uint.MaxValue+1): MSW = 0x00000001, LZC = 31 + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + // long.MaxValue (0x7FFFFFFFFFFFFFFF): MSW = 0x7FFFFFFF, LZC = 1 + Assert.Equal((BigInteger)1, BigInteger.LeadingZeroCount(BigInteger.Parse("07FFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + // 2^63 (= long.MaxValue+1): MSW = 0x80000000, LZC = 0 + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); + // ulong.MaxValue (0xFFFFFFFFFFFFFFFF): MSW = 0xFFFFFFFF, LZC = 0 + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("0FFFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + // 2^64 (= ulong.MaxValue+1): MSW = 0x00000001, LZC = 31 + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + // 2^127: MSW = 0x80000000, LZC = 0 + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("080000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + // 2^128: MSW = 0x00000001, LZC = 31 + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("0100000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + + // Large negative values always return 0. + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); + + // Results must be the same on 32-bit and 64-bit platforms. + Assert.Equal((BigInteger)32, BigInteger.LeadingZeroCount(BigInteger.Zero)); + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.One)); + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 32))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 63))); + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Pow(2, 64))); } [Fact] diff --git a/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj b/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj index defe6b20b3ec8a..540676f2d3e1af 100644 --- a/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj +++ b/src/libraries/System.Runtime.Numerics/tests/System.Runtime.Numerics.Tests.csproj @@ -21,7 +21,6 @@ - From 03facaa2ef603ced84e65a2da2474e4ca3f05da2 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 28 Mar 2026 15:45:06 -0400 Subject: [PATCH 03/20] Fix PopCount and TrailingZeroCount for platform-independent 32-bit word semantics - PopCount _sign path: nint.PopCount -> int.PopCount (fixes platform-dependent results for small negative values like -1 returning 64 on 64-bit vs 32 on 32-bit) - PopCount _bits negative path: replace inline two's complement with formula PopCount(2^W - m) = W - PopCount(m) - TZC(m) + 1 using 32-bit word width W (fixes ~nuint filling upper 32 bits with 1s on 64-bit) - TrailingZeroCount _sign path: nint.TrailingZeroCount -> int.TrailingZeroCount (fixes TZC(0) returning 64 on 64-bit vs 32 on 32-bit) - LeadingZeroCount: replace magic constant 32 with BitsPerUInt32 - Add comprehensive tests for all three methods covering _sign path, _bits path, large positive/negative values, and platform-independence invariants Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 62 +++++++++----- .../tests/BigIntegerTests.GenericMath.cs | 85 ++++++++++++++++++- 2 files changed, 125 insertions(+), 22 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index eb178cf4eab79d..8936fa55600898 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3172,7 +3172,7 @@ public static BigInteger LeadingZeroCount(BigInteger value) if (Environment.Is64BitProcess) { - uint high = (uint)(msLimb >> 32); + uint high = (uint)(msLimb >> BitsPerUInt32); msWord = (high != 0) ? high : (uint)msLimb; } else @@ -3188,7 +3188,7 @@ public static BigInteger PopCount(BigInteger value) { if (value._bits is null) { - return nint.PopCount(value._sign); + return int.PopCount(value._sign); } ulong result = 0; @@ -3205,31 +3205,53 @@ public static BigInteger PopCount(BigInteger value) } else { - // When the value is negative, we need to popcount the two's complement representation - // We'll do this "inline" to avoid needing to unnecessarily allocate. + // When the value is negative, we compute PopCount of the two's complement + // representation using 32-bit word semantics. + // + // Using the identity: PopCount(2^W - m) = W - PopCount(m) - TZC(m) + 1 + // where W is the total width in terms of 32-bit words and m is the magnitude. + // + // This avoids platform-dependent results from complementing nuint limbs, + // since ~(nuint) fills upper bits with 1s on 64-bit when the magnitude + // only uses the lower 32 bits. - int i = 0; - nuint part; + ulong magnitudePopCount = 0; + ulong magnitudeTZC = 0; + bool foundNonZero = false; - do + for (int i = 0; i < value._bits.Length; i++) { - // Simply process bits, adding the carry while the previous value is zero - - part = ~value._bits[i] + 1; - result += (ulong)BitOperations.PopCount(part); + nuint part = value._bits[i]; + magnitudePopCount += (ulong)BitOperations.PopCount(part); - i++; + if (!foundNonZero) + { + if (part == 0) + { + magnitudeTZC += (uint)BigIntegerCalculator.BitsPerLimb; + } + else + { + magnitudeTZC += (ulong)BitOperations.TrailingZeroCount(part); + foundNonZero = true; + } + } } - while ((part == 0) && (i < value._bits.Length)); - while (i < value._bits.Length) + // Compute W: total width in terms of 32-bit words. + // On 64-bit, each nuint holds two 32-bit words, except the MSL's upper + // half is excluded when it's zero (matching the original uint[] layout). + int wordCount = value._bits.Length; + if (Environment.Is64BitProcess) { - // Then process the remaining bits only utilizing the one's complement - - part = ~value._bits[i]; - result += (ulong)BitOperations.PopCount(part); - i++; + wordCount *= 2; + if ((uint)(value._bits[^1] >> BitsPerUInt32) == 0) + { + wordCount--; + } } + + result = (ulong)wordCount * BitsPerUInt32 - magnitudePopCount - magnitudeTZC + 1; } return result; @@ -3335,7 +3357,7 @@ public static BigInteger TrailingZeroCount(BigInteger value) { if (value._bits is null) { - return nint.TrailingZeroCount(value._sign); + return int.TrailingZeroCount(value._sign); } ulong result = 0; diff --git a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs index ce48f8a263dce9..28eea7f77bb2eb 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs @@ -269,10 +269,53 @@ public static void PopCountTest() Assert.Equal((BigInteger)63, BinaryIntegerHelper.PopCount(Int64MaxValue)); Assert.Equal((BigInteger)1, BinaryIntegerHelper.PopCount(Int64MinValue)); - Assert.Equal((BigInteger)(nint.Size * 8), BinaryIntegerHelper.PopCount(NegativeOne)); + Assert.Equal((BigInteger)32, BinaryIntegerHelper.PopCount(NegativeOne)); Assert.Equal((BigInteger)1, BinaryIntegerHelper.PopCount(Int64MaxValuePlusOne)); Assert.Equal((BigInteger)64, BinaryIntegerHelper.PopCount(UInt64MaxValue)); + + // Small values stored in _sign: PopCount uses 32-bit width. + Assert.Equal((BigInteger)0, BigInteger.PopCount(new BigInteger(0))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(new BigInteger(1))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(new BigInteger(2))); + Assert.Equal((BigInteger)2, BigInteger.PopCount(new BigInteger(3))); + Assert.Equal((BigInteger)31, BigInteger.PopCount(new BigInteger(int.MaxValue))); + Assert.Equal((BigInteger)32, BigInteger.PopCount(new BigInteger(-1))); + Assert.Equal((BigInteger)31, BigInteger.PopCount(new BigInteger(-2))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(new BigInteger(int.MinValue))); + + // Large positive values via _bits path. + // 2^31 (0x80000000): one bit set + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + // uint.MaxValue (0xFFFFFFFF): 32 bits set + Assert.Equal((BigInteger)32, BigInteger.PopCount(BigInteger.Parse("0FFFFFFFF", Globalization.NumberStyles.HexNumber))); + // 2^32 (0x100000000): one bit set + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + // long.MaxValue (0x7FFFFFFFFFFFFFFF): 63 bits set + Assert.Equal((BigInteger)63, BigInteger.PopCount(BigInteger.Parse("07FFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + // ulong.MaxValue (0xFFFFFFFFFFFFFFFF): 64 bits set + Assert.Equal((BigInteger)64, BigInteger.PopCount(BigInteger.Parse("0FFFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + // 2^64 (0x10000000000000000): one bit set + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + // 2^128: one bit set + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("0100000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + + // Large negative values via _bits path (two's complement). + // -(2^31): two's complement of 0x80000000 within 32 bits = 0x80000000 → PopCount = 1 + Assert.Equal((BigInteger)1, BigInteger.PopCount(-BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + // -(2^32): two's complement of [0x00000000, 0x00000001] = [0x00000000, 0xFFFFFFFF] → PopCount = 32 + Assert.Equal((BigInteger)32, BigInteger.PopCount(-BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + // -(2^64): one's complement of upper limbs = all 1s, lowest limb = 0 → PopCount depends on limb count + Assert.Equal((BigInteger)32, BigInteger.PopCount(-BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + + // Results must be the same on 32-bit and 64-bit platforms. + Assert.Equal((BigInteger)0, BigInteger.PopCount(BigInteger.Zero)); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.One)); + Assert.Equal((BigInteger)32, BigInteger.PopCount(new BigInteger(-1))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Pow(2, 32))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Pow(2, 63))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Pow(2, 64))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Pow(2, 1000))); } [Fact] @@ -398,7 +441,7 @@ public static void RotateRightTest() [Fact] public static void TrailingZeroCountTest() { - Assert.Equal((BigInteger)(nint.Size * 8), BinaryIntegerHelper.TrailingZeroCount(Zero)); + Assert.Equal((BigInteger)32, BinaryIntegerHelper.TrailingZeroCount(Zero)); Assert.Equal((BigInteger)0, BinaryIntegerHelper.TrailingZeroCount(One)); Assert.Equal((BigInteger)0, BinaryIntegerHelper.TrailingZeroCount(Int64MaxValue)); @@ -410,6 +453,44 @@ public static void TrailingZeroCountTest() Assert.Equal((BigInteger)1000, BinaryIntegerHelper.TrailingZeroCount(BigInteger.Pow(2, 1000))); Assert.Equal((BigInteger)1000, BinaryIntegerHelper.TrailingZeroCount(-BigInteger.Pow(2, 1000))); + + // Small values stored in _sign: TrailingZeroCount uses 32-bit width. + Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(new BigInteger(0))); + Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(new BigInteger(1))); + Assert.Equal((BigInteger)1, BigInteger.TrailingZeroCount(new BigInteger(2))); + Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(new BigInteger(3))); + Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(new BigInteger(int.MaxValue))); + Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(new BigInteger(-1))); + Assert.Equal((BigInteger)1, BigInteger.TrailingZeroCount(new BigInteger(-2))); + Assert.Equal((BigInteger)31, BigInteger.TrailingZeroCount(new BigInteger(int.MinValue))); + + // Large positive values via _bits path. + // 2^31 (0x80000000): 31 trailing zeros + Assert.Equal((BigInteger)31, BigInteger.TrailingZeroCount(BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + // uint.MaxValue (0xFFFFFFFF): 0 trailing zeros + Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(BigInteger.Parse("0FFFFFFFF", Globalization.NumberStyles.HexNumber))); + // 2^32 (0x100000000): 32 trailing zeros + Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + // 2^63 (0x8000000000000000): 63 trailing zeros + Assert.Equal((BigInteger)63, BigInteger.TrailingZeroCount(BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); + // 2^64 (0x10000000000000000): 64 trailing zeros + Assert.Equal((BigInteger)64, BigInteger.TrailingZeroCount(BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + // 2^128: 128 trailing zeros + Assert.Equal((BigInteger)128, BigInteger.TrailingZeroCount(BigInteger.Parse("0100000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + + // Large negative values via _bits path (two's complement shares trailing zeros with magnitude). + Assert.Equal((BigInteger)31, BigInteger.TrailingZeroCount(-BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(-BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)63, BigInteger.TrailingZeroCount(-BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)64, BigInteger.TrailingZeroCount(-BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + + // Results must be the same on 32-bit and 64-bit platforms. + Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(BigInteger.Zero)); + Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(BigInteger.One)); + Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(BigInteger.Pow(2, 32))); + Assert.Equal((BigInteger)63, BigInteger.TrailingZeroCount(BigInteger.Pow(2, 63))); + Assert.Equal((BigInteger)64, BigInteger.TrailingZeroCount(BigInteger.Pow(2, 64))); + Assert.Equal((BigInteger)1000, BigInteger.TrailingZeroCount(BigInteger.Pow(2, 1000))); } [Fact] From d9c38b9af9bf5c8ff5b8ab4ff0606d57adc2aa03 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sat, 28 Mar 2026 18:58:22 -0400 Subject: [PATCH 04/20] Simplify LeadingZeroCount _bits path per review feedback Replace the Environment.Is64BitProcess branch with the simpler BitOperations.LeadingZeroCount(value._bits[^1]) & 31 expression. The & 31 maps 64-bit LZC to 32-bit word semantics: when the upper half is zero, LZC is 32 + uint_lzc, and (32 + x) & 31 == x. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 8936fa55600898..323b19c7444a43 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3164,23 +3164,10 @@ public static BigInteger LeadingZeroCount(BigInteger value) return 0; } - // When positive, count leading zeros in the most significant 32-bit word of the value. - // On 64-bit systems, each nuint limb holds 64 bits, so we extract the most significant - // 32-bit half. This preserves the behavior from when _bits was uint[] (32-bit limbs). - nuint msLimb = value._bits[^1]; - uint msWord; - - if (Environment.Is64BitProcess) - { - uint high = (uint)(msLimb >> BitsPerUInt32); - msWord = (high != 0) ? high : (uint)msLimb; - } - else - { - msWord = (uint)msLimb; - } - - return uint.LeadingZeroCount(msWord); + // When positive, count leading zeros in the most significant 32-bit word. + // The & 31 maps the result to 32-bit word semantics: on 64-bit, when the + // upper half is zero, LZC is 32 + uint_lzc, and (32 + x) & 31 == x. + return BitOperations.LeadingZeroCount(value._bits[^1]) & 31; } /// From d9749d7e920aeec22db78b5a32a433904f929bd7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 00:18:01 +0000 Subject: [PATCH 05/20] Fix RotateLeft/RotateRight _sign path for 32-bit word semantics and update tests Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/0c384115-0987-41ef-b9d1-34ac2bb67e7d Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- dotnet-tools.json | 5 +++ .../src/System/Numerics/BigInteger.cs | 8 ++-- .../tests/BigIntegerTests.GenericMath.cs | 44 +++++++++++++++---- 3 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 dotnet-tools.json diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 00000000000000..b0e38abdace3ec --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,5 @@ +{ + "version": 1, + "isRoot": true, + "tools": {} +} \ No newline at end of file diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 323b19c7444a43..dc7d6be0a41614 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3256,9 +3256,9 @@ public static BigInteger RotateLeft(BigInteger value, int rotateAmount) if (value._bits is null) { - nuint rs = BitOperations.RotateLeft((nuint)value._sign, rotateAmount); + uint rs = uint.RotateLeft((uint)value._sign, rotateAmount); return neg - ? new BigInteger((nint)rs) + ? new BigInteger((int)rs) : new BigInteger(rs); } @@ -3277,9 +3277,9 @@ public static BigInteger RotateRight(BigInteger value, int rotateAmount) if (value._bits is null) { - nuint rs = BitOperations.RotateRight((nuint)value._sign, rotateAmount); + uint rs = uint.RotateRight((uint)value._sign, rotateAmount); return neg - ? new BigInteger((nint)rs) + ? new BigInteger((int)rs) : new BigInteger(rs); } diff --git a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs index 28eea7f77bb2eb..5c67fbbc43acfd 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs @@ -326,8 +326,8 @@ public static void RotateLeftTest() Assert.Equal((BigInteger)0x00000000, BinaryIntegerHelper.RotateLeft(Zero, 33)); Assert.Equal((BigInteger)0x00000002, BinaryIntegerHelper.RotateLeft(One, 1)); - Assert.Equal(BigInteger.One << (32 % (nint.Size * 8)), BinaryIntegerHelper.RotateLeft(One, 32)); - Assert.Equal(BigInteger.One << (33 % (nint.Size * 8)), BinaryIntegerHelper.RotateLeft(One, 33)); + Assert.Equal((BigInteger)0x00000001, BinaryIntegerHelper.RotateLeft(One, 32)); + Assert.Equal((BigInteger)0x00000002, BinaryIntegerHelper.RotateLeft(One, 33)); Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFE, BinaryIntegerHelper.RotateLeft(Int64MaxValue, 1)); Assert.Equal((BigInteger)0xFFFFFFFF7FFFFFFF, BinaryIntegerHelper.RotateLeft(Int64MaxValue, 32)); @@ -353,8 +353,8 @@ public static void RotateLeftTest() Assert.Equal((BigInteger)0x00000000, BinaryIntegerHelper.RotateLeft(Zero, -32)); Assert.Equal((BigInteger)0x00000000, BinaryIntegerHelper.RotateLeft(Zero, -33)); - Assert.Equal(BigInteger.One << (nint.Size * 8 - 1), BinaryIntegerHelper.RotateLeft(One, -1)); - Assert.Equal(BigInteger.One << ((nint.Size * 8 - 32) % (nint.Size * 8)), BinaryIntegerHelper.RotateLeft(One, -32)); + Assert.Equal((BigInteger)0x80000000, BinaryIntegerHelper.RotateLeft(One, -1)); + Assert.Equal((BigInteger)0x00000001, BinaryIntegerHelper.RotateLeft(One, -32)); Assert.Equal((BigInteger)0x80000000, BinaryIntegerHelper.RotateLeft(One, -33)); Assert.Equal((BigInteger)0xBFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateLeft(Int64MaxValue, -1)); @@ -376,6 +376,20 @@ public static void RotateLeftTest() Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateLeft(UInt64MaxValue, -1)); Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateLeft(UInt64MaxValue, -32)); Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateLeft(UInt64MaxValue, -33)); + + // Small values in _sign path: 32-bit rotation semantics. + Assert.Equal((BigInteger)0x00000004, BigInteger.RotateLeft(new BigInteger(2), 1)); + Assert.Equal((BigInteger)0x00000002, BigInteger.RotateLeft(new BigInteger(2), 32)); + Assert.Equal((BigInteger)0x00000001, BigInteger.RotateLeft(new BigInteger(2), 31)); + Assert.Equal((BigInteger)0x80000000, BigInteger.RotateLeft(new BigInteger(1), 31)); + Assert.Equal(unchecked((BigInteger)(int)0xFFFFFFFD), BigInteger.RotateLeft(new BigInteger(-2), 1)); + Assert.Equal(unchecked((BigInteger)(int)0xFFFFFFFE), BigInteger.RotateLeft(new BigInteger(-2), 32)); + + // Platform-independence: results must be the same on 32-bit and 64-bit. + Assert.Equal((BigInteger)0x00000001, BigInteger.RotateLeft(BigInteger.One, 32)); + Assert.Equal((BigInteger)0x80000000, BigInteger.RotateLeft(BigInteger.One, -1)); + Assert.Equal(BigInteger.MinusOne, BigInteger.RotateLeft(BigInteger.MinusOne, 1)); + Assert.Equal(BigInteger.MinusOne, BigInteger.RotateLeft(BigInteger.MinusOne, 32)); } [Fact] @@ -385,8 +399,8 @@ public static void RotateRightTest() Assert.Equal((BigInteger)0x00000000, BinaryIntegerHelper.RotateRight(Zero, 32)); Assert.Equal((BigInteger)0x00000000, BinaryIntegerHelper.RotateRight(Zero, 33)); - Assert.Equal(BigInteger.One << (nint.Size * 8 - 1), BinaryIntegerHelper.RotateRight(One, 1)); - Assert.Equal(BigInteger.One << ((nint.Size * 8 - 32) % (nint.Size * 8)), BinaryIntegerHelper.RotateRight(One, 32)); + Assert.Equal((BigInteger)0x80000000, BinaryIntegerHelper.RotateRight(One, 1)); + Assert.Equal((BigInteger)0x00000001, BinaryIntegerHelper.RotateRight(One, 32)); Assert.Equal((BigInteger)0x80000000, BinaryIntegerHelper.RotateRight(One, 33)); Assert.Equal((BigInteger)0xBFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateRight(Int64MaxValue, 1)); @@ -414,8 +428,8 @@ public static void RotateRightTest() Assert.Equal((BigInteger)0x00000000, BinaryIntegerHelper.RotateRight(Zero, -33)); Assert.Equal((BigInteger)0x00000002, BinaryIntegerHelper.RotateRight(One, -1)); - Assert.Equal(BigInteger.One << (32 % (nint.Size * 8)), BinaryIntegerHelper.RotateRight(One, -32)); - Assert.Equal(BigInteger.One << (33 % (nint.Size * 8)), BinaryIntegerHelper.RotateRight(One, -33)); + Assert.Equal((BigInteger)0x00000001, BinaryIntegerHelper.RotateRight(One, -32)); + Assert.Equal((BigInteger)0x00000002, BinaryIntegerHelper.RotateRight(One, -33)); Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFE, BinaryIntegerHelper.RotateRight(Int64MaxValue, -1)); Assert.Equal((BigInteger)0xFFFFFFFF7FFFFFFF, BinaryIntegerHelper.RotateRight(Int64MaxValue, -32)); @@ -436,6 +450,20 @@ public static void RotateRightTest() Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateRight(UInt64MaxValue, -1)); Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateRight(UInt64MaxValue, -32)); Assert.Equal((BigInteger)0xFFFFFFFFFFFFFFFF, BinaryIntegerHelper.RotateRight(UInt64MaxValue, -33)); + + // Small values in _sign path: 32-bit rotation semantics. + Assert.Equal((BigInteger)0x00000001, BigInteger.RotateRight(new BigInteger(2), 1)); + Assert.Equal((BigInteger)0x00000002, BigInteger.RotateRight(new BigInteger(2), 32)); + Assert.Equal((BigInteger)0x00000004, BigInteger.RotateRight(new BigInteger(2), 31)); + Assert.Equal((BigInteger)0x00000002, BigInteger.RotateRight(new BigInteger(1), 31)); + Assert.Equal(unchecked((BigInteger)(int)0xBFFFFFFF), BigInteger.RotateRight(new BigInteger(-2), 1)); + Assert.Equal(unchecked((BigInteger)(int)0xFFFFFFFE), BigInteger.RotateRight(new BigInteger(-2), 32)); + + // Platform-independence: results must be the same on 32-bit and 64-bit. + Assert.Equal((BigInteger)0x00000001, BigInteger.RotateRight(BigInteger.One, 32)); + Assert.Equal((BigInteger)0x80000000, BigInteger.RotateRight(BigInteger.One, 1)); + Assert.Equal(BigInteger.MinusOne, BigInteger.RotateRight(BigInteger.MinusOne, 1)); + Assert.Equal(BigInteger.MinusOne, BigInteger.RotateRight(BigInteger.MinusOne, 32)); } [Fact] From b52efac0f903154d1c684afbf6dc0bd785280293 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:02:09 +0000 Subject: [PATCH 06/20] Fix Rotate _bits path for 32-bit word semantics and update tests Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/fe4dea75-2b81-4f70-8170-f1cde9b676e0 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 86 ++++++++--- .../Numerics/BigIntegerCalculator.ShiftRot.cs | 134 ++++++++++++++++++ .../tests/BigInteger/MyBigInt.cs | 2 +- .../tests/BigInteger/Rotate.cs | 16 +-- .../tests/BigIntegerTests.GenericMath.cs | 2 +- 5 files changed, 208 insertions(+), 32 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index dc7d6be0a41614..352401d5f7adb0 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3291,41 +3291,83 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r Debug.Assert(bits.Length > 0); Debug.Assert(Math.Abs(rotateLeftAmount) <= 0x80000000); - int zLength = bits.Length; - int leadingZeroCount = negative ? bits.IndexOfAnyExcept(0u) : 0; + // Determine the number of 32-bit words in the magnitude. + // On 64-bit, each nuint limb holds two 32-bit words; the MSL's upper + // half may be zero (matching the original uint[] layout). + int wordCount; + if (Environment.Is64BitProcess) + { + wordCount = bits.Length * 2; + if ((uint)(bits[^1] >> BitsPerUInt32) == 0) + { + wordCount--; + } + } + else + { + wordCount = bits.Length; + } + + int zWordCount = wordCount; + + // Reinterpret magnitude as a span of 32-bit words. + ReadOnlySpan words = MemoryMarshal.Cast(bits).Slice(0, wordCount); - if (negative && (nint)bits[^1] < 0 - && (leadingZeroCount != bits.Length - 1 || bits[^1] != ((nuint)1 << (BigIntegerCalculator.BitsPerLimb - 1)))) + int leadingZeroCount = negative ? words.IndexOfAnyExcept(0u) : 0; + + if (negative && (int)words[^1] < 0 + && (leadingZeroCount != words.Length - 1 || words[^1] != UInt32HighBit)) { - // For a shift of N x BitsPerLimb bit, - // We check for a special case where its sign bit could be outside the nuint array after 2's complement conversion. - // For example given [nuint.MaxValue, nuint.MaxValue, nuint.MaxValue], its 2's complement is [0x01, 0x00, 0x00] - // After a BitsPerLimb bit right shift, it becomes [0x00, 0x00] which is [0x00, 0x00] when converted back. - // The expected result is [0x00, 0x00, nuint.MaxValue] (2's complement) or [0x00, 0x00, 0x01] when converted back - // If the 2's component's last element is a 0, we will track the sign externally - ++zLength; + // For a shift of N x 32 bit, + // We check for a special case where its sign bit could be outside the uint array after 2's complement conversion. + // For example given [0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF], its 2's complement is [0x01, 0x00, 0x00] + // After a 32 bit right shift, it becomes [0x00, 0x00] which is [0x00, 0x00] when converted back. + // The expected result is [0x00, 0x00, 0xFFFFFFFF] (2's complement) or [0x00, 0x00, 0x01] when converted back + // If the 2's complement's last element is a 0, we will track the sign externally + ++zWordCount; } - Span zd = RentedBuffer.Create(zLength, out RentedBuffer zdBuffer); + // Allocate a buffer of nuint limbs large enough to hold zWordCount 32-bit words. + int zLimbCount = (zWordCount + (Environment.Is64BitProcess ? 1 : 0)) / (Environment.Is64BitProcess ? 2 : 1); + Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); + zd[^1] = 0; // Clear the last nuint limb (which may be partially used) - zd[^1] = 0; - bits.CopyTo(zd); + // Work on the 32-bit word view of the buffer. + Span zw = MemoryMarshal.Cast(zd).Slice(0, zWordCount); + + // Copy magnitude words into the working buffer. + words.CopyTo(zw); + if (zWordCount > wordCount) + { + zw[^1] = 0; // Extra word for sign tracking + } if (negative) { - Debug.Assert((uint)leadingZeroCount < (uint)zd.Length); + Debug.Assert((uint)leadingZeroCount < (uint)zw.Length); - // Same as NumericsHelpers.DangerousMakeTwosComplement(zd); - // Leading zero count is already calculated. - zd[leadingZeroCount] = (nuint)(-(nint)zd[leadingZeroCount]); - NumericsHelpers.DangerousMakeOnesComplement(zd.Slice(leadingZeroCount + 1)); + // Two's complement conversion on the 32-bit word view. + zw[leadingZeroCount] = (uint)(-(int)zw[leadingZeroCount]); + for (int i = leadingZeroCount + 1; i < zw.Length; i++) + { + zw[i] = ~zw[i]; + } } - BigIntegerCalculator.RotateLeft(zd, rotateLeftAmount); + BigIntegerCalculator.RotateLeft32(zw, rotateLeftAmount); - if (negative && (nint)zd[^1] < 0) + if (negative && (int)zw[^1] < 0) { - NumericsHelpers.DangerousMakeTwosComplement(zd); + // Convert back from two's complement on the 32-bit word view. + int i = zw.IndexOfAnyExcept(0u); + if ((uint)i < (uint)zw.Length) + { + zw[i] = (uint)(-(int)zw[i]); + for (int j = i + 1; j < zw.Length; j++) + { + zw[j] = ~zw[j]; + } + } } else { diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index 0ea6e11b122f5d..ba385593143a61 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -9,6 +9,140 @@ namespace System.Numerics { internal static partial class BigIntegerCalculator { + /// + /// Rotates a span of 32-bit words left by the specified amount. + /// This provides platform-independent 32-bit word rotation semantics. + /// + public static void RotateLeft32(Span bits, long rotateLeftAmount) + { + Debug.Assert(Math.Abs(rotateLeftAmount) <= 0x80000000); + + const int BitsPerWord = 32; + int digitShiftMax = (int)(0x80000000 / BitsPerWord); + + int digitShift = digitShiftMax; + int smallShift = 0; + + if (rotateLeftAmount < 0) + { + if (rotateLeftAmount != -0x80000000) + { + (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerWord); + } + + RotateRight32(bits, digitShift % bits.Length, smallShift); + } + else + { + if (rotateLeftAmount != 0x80000000) + { + (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerWord); + } + + RotateLeft32(bits, digitShift % bits.Length, smallShift); + } + } + + private static void RotateLeft32(Span bits, int digitShift, int smallShift) + { + Debug.Assert(bits.Length > 0); + + LeftShiftSelf32(bits, smallShift, out uint carry); + bits[0] |= carry; + + if (digitShift == 0) + { + return; + } + + SwapUpperAndLower32(bits, bits.Length - digitShift); + } + + private static void RotateRight32(Span bits, int digitShift, int smallShift) + { + Debug.Assert(bits.Length > 0); + + RightShiftSelf32(bits, smallShift, out uint carry); + bits[^1] |= carry; + + if (digitShift == 0) + { + return; + } + + SwapUpperAndLower32(bits, digitShift); + } + + private static void SwapUpperAndLower32(Span bits, int lowerLength) + { + Debug.Assert(lowerLength > 0); + Debug.Assert(lowerLength < bits.Length); + + int upperLength = bits.Length - lowerLength; + + Span lower = bits.Slice(0, lowerLength); + Span upper = bits.Slice(lowerLength); + + Span lowerDst = bits.Slice(upperLength); + + int tmpLength = Math.Min(lowerLength, upperLength); + uint[] tmpArray = new uint[tmpLength]; + Span tmp = tmpArray; + + if (upperLength < lowerLength) + { + upper.CopyTo(tmp); + lower.CopyTo(lowerDst); + tmp.CopyTo(bits); + } + else + { + lower.CopyTo(tmp); + upper.CopyTo(bits); + tmp.CopyTo(lowerDst); + } + } + + private static void LeftShiftSelf32(Span bits, int shift, out uint carry) + { + Debug.Assert((uint)shift < 32); + + carry = 0; + if (shift == 0 || bits.IsEmpty) + { + return; + } + + int back = 32 - shift; + carry = bits[^1] >> back; + + for (int i = bits.Length - 1; i > 0; i--) + { + bits[i] = (bits[i] << shift) | (bits[i - 1] >> back); + } + bits[0] <<= shift; + } + + private static void RightShiftSelf32(Span bits, int shift, out uint carry) + { + Debug.Assert((uint)shift < 32); + + carry = 0; + if (shift == 0 || bits.IsEmpty) + { + return; + } + + int back = 32 - shift; + carry = bits[0] << back; + + for (int i = 0; i < bits.Length - 1; i++) + { + bits[i] = (bits[i] >> shift) | (bits[i + 1] << back); + } + bits[^1] >>= shift; + } + public static void RotateLeft(Span bits, long rotateLeftAmount) { Debug.Assert(Math.Abs(rotateLeftAmount) <= 0x80000000); diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/MyBigInt.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/MyBigInt.cs index 1490d14c7dc152..21143a8f9a901e 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/MyBigInt.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/MyBigInt.cs @@ -900,7 +900,7 @@ public static List RotateLeft(List bytes1, List bytes2) if (fill == 0 && bytes1.Count > 1 && bytes1[bytes1.Count - 1] == 0) bytes1.RemoveAt(bytes1.Count - 1); - while (bytes1.Count % nint.Size != 0) + while (bytes1.Count % 4 != 0) { bytes1.Add(fill); } diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/Rotate.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/Rotate.cs index ea5136eba80506..f90d17ac50825c 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/Rotate.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/Rotate.cs @@ -281,7 +281,7 @@ private static byte[] GetRandomNegByteArray(Random random, int size) private static byte[] GetRandomLengthAllOnesUIntByteArray(Random random) { int gap = random.Next(0, 128); - int byteLength = nint.Size + gap * nint.Size + 1; + int byteLength = 4 + gap * 4 + 1; byte[] array = new byte[byteLength]; array[0] = 1; array[^1] = 0xFF; @@ -290,9 +290,9 @@ private static byte[] GetRandomLengthAllOnesUIntByteArray(Random random) private static byte[] GetRandomLengthFirstUIntMaxSecondUIntMSBMaxArray(Random random) { int gap = random.Next(0, 128); - int byteLength = nint.Size + gap * nint.Size + 1; + int byteLength = 4 + gap * 4 + 1; byte[] array = new byte[byteLength]; - array[^(nint.Size + 1)] = 0x80; + array[^(4 + 1)] = 0x80; array[^1] = 0xFF; return array; } @@ -310,7 +310,7 @@ public class RotateLeftTest : RotateTestBase public static TheoryData NegativeNumber_TestData() { - int bpl = nint.Size * 8; // bits per limb + int bpl = 32; // bits per word BigInteger neg2 = -(BigInteger.One << (3 * bpl - 1)); // -2^(3*bpl-1) BigInteger neg1 = neg2 + 1; // -(2^(3*bpl-1) - 1) BigInteger neg3 = neg2 + 2; // -(2^(3*bpl-1) - 2) @@ -433,7 +433,7 @@ public void NegativeNumber(BigInteger input, int rotateAmount, BigInteger expect [Fact] public void PowerOfTwo() { - int bpl = nint.Size * 8; // bits per limb + int bpl = 32; // bits per word for (int i = 0; i < bpl; i++) { foreach (int k in new int[] { 1, 2, 3, 10 }) @@ -445,7 +445,7 @@ public void PowerOfTwo() Assert.Equal(BigInteger.One << i, BigInteger.RotateLeft(plus, bpl)); Assert.Equal(BigInteger.One << (bpl * (k - 1) + i), BigInteger.RotateLeft(plus, bpl * k)); - Assert.Equal(i == bpl - 1 ? BigInteger.One : (new BigInteger((nint)(-1) << (i + 1)) << bpl * k) + 1, + Assert.Equal(i == bpl - 1 ? BigInteger.One : (new BigInteger((int)(-1) << (i + 1)) << bpl * k) + 1, BigInteger.RotateLeft(minus, 1)); Assert.Equal((BigInteger.One << bpl) - (BigInteger.One << i), BigInteger.RotateLeft(minus, bpl)); Assert.Equal(((BigInteger.One << bpl) - (BigInteger.One << i)) << (bpl * (k - 1)), BigInteger.RotateLeft(minus, bpl * k)); @@ -460,7 +460,7 @@ public class RotateRightTest : RotateTestBase public static TheoryData NegativeNumber_TestData() { - int bpl = nint.Size * 8; // bits per limb + int bpl = 32; // bits per word BigInteger neg2 = -(BigInteger.One << (3 * bpl - 1)); // -2^(3*bpl-1) BigInteger neg1 = neg2 + 1; // -(2^(3*bpl-1) - 1) BigInteger neg3 = neg2 + 2; // -(2^(3*bpl-1) - 2) @@ -583,7 +583,7 @@ public void NegativeNumber(BigInteger input, int rotateAmount, BigInteger expect [Fact] public void PowerOfTwo() { - int bpl = nint.Size * 8; // bits per limb + int bpl = 32; // bits per word for (int i = 0; i < bpl; i++) { foreach (int k in new int[] { 1, 2, 3, 10 }) diff --git a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs index 5c67fbbc43acfd..32bce6c8648ad3 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs @@ -456,7 +456,7 @@ public static void RotateRightTest() Assert.Equal((BigInteger)0x00000002, BigInteger.RotateRight(new BigInteger(2), 32)); Assert.Equal((BigInteger)0x00000004, BigInteger.RotateRight(new BigInteger(2), 31)); Assert.Equal((BigInteger)0x00000002, BigInteger.RotateRight(new BigInteger(1), 31)); - Assert.Equal(unchecked((BigInteger)(int)0xBFFFFFFF), BigInteger.RotateRight(new BigInteger(-2), 1)); + Assert.Equal(new BigInteger(int.MaxValue), BigInteger.RotateRight(new BigInteger(-2), 1)); Assert.Equal(unchecked((BigInteger)(int)0xFFFFFFFE), BigInteger.RotateRight(new BigInteger(-2), 32)); // Platform-independence: results must be the same on 32-bit and 64-bit. From b57835dd68edebfd476b7b18231ddf6c2d0046ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:05:26 +0000 Subject: [PATCH 07/20] Fix SwapUpperAndLower32 to use ArrayPool, simplify zLimbCount, remove accidental dotnet-tools.json Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/fe4dea75-2b81-4f70-8170-f1cde9b676e0 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- dotnet-tools.json | 5 ----- .../src/System/Numerics/BigInteger.cs | 3 ++- .../src/System/Numerics/BigIntegerCalculator.ShiftRot.cs | 6 ++++-- 3 files changed, 6 insertions(+), 8 deletions(-) delete mode 100644 dotnet-tools.json diff --git a/dotnet-tools.json b/dotnet-tools.json deleted file mode 100644 index b0e38abdace3ec..00000000000000 --- a/dotnet-tools.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": {} -} \ No newline at end of file diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 352401d5f7adb0..8cedf42b178919 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3328,7 +3328,8 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r } // Allocate a buffer of nuint limbs large enough to hold zWordCount 32-bit words. - int zLimbCount = (zWordCount + (Environment.Is64BitProcess ? 1 : 0)) / (Environment.Is64BitProcess ? 2 : 1); + // On 64-bit, each nuint limb holds 2 words, so we need ceil(zWordCount / 2) limbs. + int zLimbCount = Environment.Is64BitProcess ? (zWordCount + 1) / 2 : zWordCount; Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); zd[^1] = 0; // Clear the last nuint limb (which may be partially used) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index ba385593143a61..5f891ae817ad70 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -86,8 +86,8 @@ private static void SwapUpperAndLower32(Span bits, int lowerLength) Span lowerDst = bits.Slice(upperLength); int tmpLength = Math.Min(lowerLength, upperLength); - uint[] tmpArray = new uint[tmpLength]; - Span tmp = tmpArray; + uint[] tmpArray = System.Buffers.ArrayPool.Shared.Rent(tmpLength); + Span tmp = tmpArray.AsSpan(0, tmpLength); if (upperLength < lowerLength) { @@ -101,6 +101,8 @@ private static void SwapUpperAndLower32(Span bits, int lowerLength) upper.CopyTo(bits); tmp.CopyTo(lowerDst); } + + System.Buffers.ArrayPool.Shared.Return(tmpArray); } private static void LeftShiftSelf32(Span bits, int shift, out uint carry) From 77e8bdd8edfa53425c163a64772826a79c8c805e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:45:49 +0000 Subject: [PATCH 08/20] Fix Rotate endianness: replace MemoryMarshal.Cast with arithmetic word extraction Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/f9c9d491-2692-46b0-935b-ba460d821007 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 112 ++++++++++++++---- 1 file changed, 86 insertions(+), 26 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 8cedf42b178919..bc32cfe4625c2a 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3308,15 +3308,25 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r wordCount = bits.Length; } - int zWordCount = wordCount; + // Extract 32-bit words from nuint limbs using arithmetic (endianness-safe). + // Word 0 = low 32 bits of limb 0, word 1 = high 32 bits of limb 0 (on 64-bit), etc. + uint[] zw = new uint[wordCount]; + ExtractWords(bits, zw); - // Reinterpret magnitude as a span of 32-bit words. - ReadOnlySpan words = MemoryMarshal.Cast(bits).Slice(0, wordCount); + int zWordCount = wordCount; - int leadingZeroCount = negative ? words.IndexOfAnyExcept(0u) : 0; + // For negative values, find the index of the first non-zero word. + int leadingZeroCount = 0; + if (negative) + { + while (leadingZeroCount < zw.Length && zw[leadingZeroCount] == 0) + { + leadingZeroCount++; + } + } - if (negative && (int)words[^1] < 0 - && (leadingZeroCount != words.Length - 1 || words[^1] != UInt32HighBit)) + if (negative && (int)zw[zWordCount - 1] < 0 + && (leadingZeroCount != zWordCount - 1 || zw[zWordCount - 1] != UInt32HighBit)) { // For a shift of N x 32 bit, // We check for a special case where its sign bit could be outside the uint array after 2's complement conversion. @@ -3325,22 +3335,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r // The expected result is [0x00, 0x00, 0xFFFFFFFF] (2's complement) or [0x00, 0x00, 0x01] when converted back // If the 2's complement's last element is a 0, we will track the sign externally ++zWordCount; - } - - // Allocate a buffer of nuint limbs large enough to hold zWordCount 32-bit words. - // On 64-bit, each nuint limb holds 2 words, so we need ceil(zWordCount / 2) limbs. - int zLimbCount = Environment.Is64BitProcess ? (zWordCount + 1) / 2 : zWordCount; - Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); - zd[^1] = 0; // Clear the last nuint limb (which may be partially used) - - // Work on the 32-bit word view of the buffer. - Span zw = MemoryMarshal.Cast(zd).Slice(0, zWordCount); - - // Copy magnitude words into the working buffer. - words.CopyTo(zw); - if (zWordCount > wordCount) - { - zw[^1] = 0; // Extra word for sign tracking + Array.Resize(ref zw, zWordCount); } if (negative) @@ -3360,11 +3355,16 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r if (negative && (int)zw[^1] < 0) { // Convert back from two's complement on the 32-bit word view. - int i = zw.IndexOfAnyExcept(0u); - if ((uint)i < (uint)zw.Length) + int firstNonZero = 0; + while (firstNonZero < zw.Length && zw[firstNonZero] == 0) { - zw[i] = (uint)(-(int)zw[i]); - for (int j = i + 1; j < zw.Length; j++) + firstNonZero++; + } + + if ((uint)firstNonZero < (uint)zw.Length) + { + zw[firstNonZero] = (uint)(-(int)zw[firstNonZero]); + for (int j = firstNonZero + 1; j < zw.Length; j++) { zw[j] = ~zw[j]; } @@ -3375,6 +3375,12 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r negative = false; } + // Pack 32-bit words back into nuint limbs (endianness-safe). + int zLimbCount = Environment.Is64BitProcess ? (zWordCount + 1) / 2 : zWordCount; + Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); + zd[^1] = 0; + PackWords(zw, zd); + BigInteger result = new(zd, negative); zdBuffer.Dispose(); @@ -3382,6 +3388,60 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r return result; } + /// + /// Extracts 32-bit words from nuint limbs using arithmetic operations (endianness-safe). + /// Word 0 is the least significant 32 bits of limb 0. + /// + private static void ExtractWords(ReadOnlySpan limbs, Span words) + { + if (Environment.Is64BitProcess) + { + int w = 0; + for (int i = 0; i < limbs.Length; i++) + { + words[w++] = (uint)limbs[i]; + if (w < words.Length) + { + words[w++] = (uint)(limbs[i] >> BitsPerUInt32); + } + } + } + else + { + for (int i = 0; i < words.Length; i++) + { + words[i] = (uint)limbs[i]; + } + } + } + + /// + /// Packs 32-bit words back into nuint limbs using arithmetic operations (endianness-safe). + /// + private static void PackWords(ReadOnlySpan words, Span limbs) + { + if (Environment.Is64BitProcess) + { + int w = 0; + for (int i = 0; i < limbs.Length; i++) + { + nuint limb = words[w++]; + if (w < words.Length) + { + limb |= (nuint)words[w++] << BitsPerUInt32; + } + limbs[i] = limb; + } + } + else + { + for (int i = 0; i < words.Length; i++) + { + limbs[i] = words[i]; + } + } + } + /// public static BigInteger TrailingZeroCount(BigInteger value) { From 57e132262ac6510c343af87fe7276305e7667976 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:52:23 +0000 Subject: [PATCH 09/20] Remove dead nuint-based Rotate helpers, avoid Array.Resize in Rotate Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/f9c9d491-2692-46b0-935b-ba460d821007 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 26 +++--- .../Numerics/BigIntegerCalculator.ShiftRot.cs | 90 ------------------- 2 files changed, 14 insertions(+), 102 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index bc32cfe4625c2a..c371001cfedf76 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3310,8 +3310,9 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r // Extract 32-bit words from nuint limbs using arithmetic (endianness-safe). // Word 0 = low 32 bits of limb 0, word 1 = high 32 bits of limb 0 (on 64-bit), etc. - uint[] zw = new uint[wordCount]; - ExtractWords(bits, zw); + // Allocate one extra word up front for the possible sign-extension word (avoids Array.Resize). + uint[] zw = new uint[wordCount + 1]; + ExtractWords(bits, zw.AsSpan(0, wordCount)); int zWordCount = wordCount; @@ -3319,7 +3320,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r int leadingZeroCount = 0; if (negative) { - while (leadingZeroCount < zw.Length && zw[leadingZeroCount] == 0) + while (leadingZeroCount < wordCount && zw[leadingZeroCount] == 0) { leadingZeroCount++; } @@ -3335,36 +3336,37 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r // The expected result is [0x00, 0x00, 0xFFFFFFFF] (2's complement) or [0x00, 0x00, 0x01] when converted back // If the 2's complement's last element is a 0, we will track the sign externally ++zWordCount; - Array.Resize(ref zw, zWordCount); } if (negative) { - Debug.Assert((uint)leadingZeroCount < (uint)zw.Length); + Debug.Assert((uint)leadingZeroCount < (uint)zWordCount); // Two's complement conversion on the 32-bit word view. zw[leadingZeroCount] = (uint)(-(int)zw[leadingZeroCount]); - for (int i = leadingZeroCount + 1; i < zw.Length; i++) + for (int i = leadingZeroCount + 1; i < zWordCount; i++) { zw[i] = ~zw[i]; } } - BigIntegerCalculator.RotateLeft32(zw, rotateLeftAmount); + Span zwSpan = zw.AsSpan(0, zWordCount); - if (negative && (int)zw[^1] < 0) + BigIntegerCalculator.RotateLeft32(zwSpan, rotateLeftAmount); + + if (negative && (int)zwSpan[^1] < 0) { // Convert back from two's complement on the 32-bit word view. int firstNonZero = 0; - while (firstNonZero < zw.Length && zw[firstNonZero] == 0) + while (firstNonZero < zWordCount && zw[firstNonZero] == 0) { firstNonZero++; } - if ((uint)firstNonZero < (uint)zw.Length) + if ((uint)firstNonZero < (uint)zWordCount) { zw[firstNonZero] = (uint)(-(int)zw[firstNonZero]); - for (int j = firstNonZero + 1; j < zw.Length; j++) + for (int j = firstNonZero + 1; j < zWordCount; j++) { zw[j] = ~zw[j]; } @@ -3379,7 +3381,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r int zLimbCount = Environment.Is64BitProcess ? (zWordCount + 1) / 2 : zWordCount; Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); zd[^1] = 0; - PackWords(zw, zd); + PackWords(zwSpan, zd); BigInteger result = new(zd, negative); diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index 5f891ae817ad70..dea541dca164a7 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -145,96 +145,6 @@ private static void RightShiftSelf32(Span bits, int shift, out uint carry) bits[^1] >>= shift; } - public static void RotateLeft(Span bits, long rotateLeftAmount) - { - Debug.Assert(Math.Abs(rotateLeftAmount) <= 0x80000000); - - int digitShiftMax = (int)(0x80000000 / BitsPerLimb); - - int digitShift = digitShiftMax; - int smallShift = 0; - - if (rotateLeftAmount < 0) - { - if (rotateLeftAmount != -0x80000000) - { - (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerLimb); - } - - RotateRight(bits, digitShift % bits.Length, smallShift); - } - else - { - if (rotateLeftAmount != 0x80000000) - { - (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerLimb); - } - - RotateLeft(bits, digitShift % bits.Length, smallShift); - } - } - - public static void RotateLeft(Span bits, int digitShift, int smallShift) - { - Debug.Assert(bits.Length > 0); - - LeftShiftSelf(bits, smallShift, out nuint carry); - bits[0] |= carry; - - if (digitShift == 0) - { - return; - } - - SwapUpperAndLower(bits, bits.Length - digitShift); - } - - public static void RotateRight(Span bits, int digitShift, int smallShift) - { - Debug.Assert(bits.Length > 0); - - RightShiftSelf(bits, smallShift, out nuint carry); - bits[^1] |= carry; - - if (digitShift == 0) - { - return; - } - - SwapUpperAndLower(bits, digitShift); - } - - private static void SwapUpperAndLower(Span bits, int lowerLength) - { - Debug.Assert(lowerLength > 0); - Debug.Assert(lowerLength < bits.Length); - - int upperLength = bits.Length - lowerLength; - - Span lower = bits.Slice(0, lowerLength); - Span upper = bits.Slice(lowerLength); - - Span lowerDst = bits.Slice(upperLength); - - int tmpLength = Math.Min(lowerLength, upperLength); - Span tmp = BigInteger.RentedBuffer.Create(tmpLength, out BigInteger.RentedBuffer tmpBuffer); - - if (upperLength < lowerLength) - { - upper.CopyTo(tmp); - lower.CopyTo(lowerDst); - tmp.CopyTo(bits); - } - else - { - lower.CopyTo(tmp); - upper.CopyTo(bits); - tmp.CopyTo(lowerDst); - } - - tmpBuffer.Dispose(); - } - public static void LeftShiftSelf(Span bits, int shift, out nuint carry) { Debug.Assert((uint)shift < BitsPerLimb); From b7b342a68247fdb1bd776c9ec2e8095debd3cfee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:56:35 +0000 Subject: [PATCH 10/20] Use IndexOfAnyExcept for first-non-zero word search in Rotate Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/f9c9d491-2692-46b0-935b-ba460d821007 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index c371001cfedf76..622c5adff53997 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3317,14 +3317,8 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r int zWordCount = wordCount; // For negative values, find the index of the first non-zero word. - int leadingZeroCount = 0; - if (negative) - { - while (leadingZeroCount < wordCount && zw[leadingZeroCount] == 0) - { - leadingZeroCount++; - } - } + Span wordsSpan = zw.AsSpan(0, wordCount); + int leadingZeroCount = negative ? wordsSpan.IndexOfAnyExcept(0u) : 0; if (negative && (int)zw[zWordCount - 1] < 0 && (leadingZeroCount != zWordCount - 1 || zw[zWordCount - 1] != UInt32HighBit)) @@ -3357,11 +3351,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r if (negative && (int)zwSpan[^1] < 0) { // Convert back from two's complement on the 32-bit word view. - int firstNonZero = 0; - while (firstNonZero < zWordCount && zw[firstNonZero] == 0) - { - firstNonZero++; - } + int firstNonZero = zwSpan.IndexOfAnyExcept(0u); if ((uint)firstNonZero < (uint)zWordCount) { From 5284673c733d9e696f02d1b68bc9f0a52e8a706d Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 29 Mar 2026 07:34:16 -0400 Subject: [PATCH 11/20] Update src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs Co-authored-by: Jan Kotas --- .../System.Runtime.Numerics/src/System/Numerics/BigInteger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 622c5adff53997..75add6d91689e5 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3310,7 +3310,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r // Extract 32-bit words from nuint limbs using arithmetic (endianness-safe). // Word 0 = low 32 bits of limb 0, word 1 = high 32 bits of limb 0 (on 64-bit), etc. - // Allocate one extra word up front for the possible sign-extension word (avoids Array.Resize). + // Allocate one extra word up front for the possible sign-extension word. uint[] zw = new uint[wordCount + 1]; ExtractWords(bits, zw.AsSpan(0, wordCount)); From f016fd43f1fd71918b1fffac3512df78464ac068 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:08:20 +0000 Subject: [PATCH 12/20] Optimize Rotate() to avoid temporary uint[] array allocation Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/9dccdcf0-037c-42a1-9f2b-1324a719e005 Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 112 +++++++----------- 1 file changed, 40 insertions(+), 72 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 75add6d91689e5..adc39d0608c83b 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3308,20 +3308,38 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r wordCount = bits.Length; } - // Extract 32-bit words from nuint limbs using arithmetic (endianness-safe). - // Word 0 = low 32 bits of limb 0, word 1 = high 32 bits of limb 0 (on 64-bit), etc. - // Allocate one extra word up front for the possible sign-extension word. - uint[] zw = new uint[wordCount + 1]; - ExtractWords(bits, zw.AsSpan(0, wordCount)); + // Allocate the result buffer with one extra word for possible sign-extension, + // and work directly in it to avoid a temporary array. + int maxWordCount = wordCount + 1; + int zLimbCount = Environment.Is64BitProcess ? (maxWordCount + 1) / 2 : maxWordCount; + Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); + + // Copy input magnitude and zero any extra limbs for sign extension / partial last limb. + bits.CopyTo(zd); + zd.Slice(bits.Length).Clear(); + + // On big-endian 64-bit, swap uint halves within each limb so that + // MemoryMarshal.Cast yields words in low-to-high order. + if (Environment.Is64BitProcess && !BitConverter.IsLittleEndian) + { + for (int i = 0; i < zd.Length; i++) + { + nuint limb = zd[i]; + zd[i] = ((limb & 0xFFFFFFFF) << BitsPerUInt32) | (limb >> BitsPerUInt32); + } + } + + // Get a Span working view directly into the nuint buffer. + Span allWords = MemoryMarshal.Cast(zd); int zWordCount = wordCount; // For negative values, find the index of the first non-zero word. - Span wordsSpan = zw.AsSpan(0, wordCount); + Span wordsSpan = allWords.Slice(0, wordCount); int leadingZeroCount = negative ? wordsSpan.IndexOfAnyExcept(0u) : 0; - if (negative && (int)zw[zWordCount - 1] < 0 - && (leadingZeroCount != zWordCount - 1 || zw[zWordCount - 1] != UInt32HighBit)) + if (negative && (int)allWords[zWordCount - 1] < 0 + && (leadingZeroCount != zWordCount - 1 || allWords[zWordCount - 1] != UInt32HighBit)) { // For a shift of N x 32 bit, // We check for a special case where its sign bit could be outside the uint array after 2's complement conversion. @@ -3337,14 +3355,14 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r Debug.Assert((uint)leadingZeroCount < (uint)zWordCount); // Two's complement conversion on the 32-bit word view. - zw[leadingZeroCount] = (uint)(-(int)zw[leadingZeroCount]); + allWords[leadingZeroCount] = (uint)(-(int)allWords[leadingZeroCount]); for (int i = leadingZeroCount + 1; i < zWordCount; i++) { - zw[i] = ~zw[i]; + allWords[i] = ~allWords[i]; } } - Span zwSpan = zw.AsSpan(0, zWordCount); + Span zwSpan = allWords.Slice(0, zWordCount); BigIntegerCalculator.RotateLeft32(zwSpan, rotateLeftAmount); @@ -3355,10 +3373,10 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r if ((uint)firstNonZero < (uint)zWordCount) { - zw[firstNonZero] = (uint)(-(int)zw[firstNonZero]); + allWords[firstNonZero] = (uint)(-(int)allWords[firstNonZero]); for (int j = firstNonZero + 1; j < zWordCount; j++) { - zw[j] = ~zw[j]; + allWords[j] = ~allWords[j]; } } } @@ -3367,11 +3385,15 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r negative = false; } - // Pack 32-bit words back into nuint limbs (endianness-safe). - int zLimbCount = Environment.Is64BitProcess ? (zWordCount + 1) / 2 : zWordCount; - Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); - zd[^1] = 0; - PackWords(zwSpan, zd); + // On big-endian 64-bit, swap uint halves back to restore correct nuint layout. + if (Environment.Is64BitProcess && !BitConverter.IsLittleEndian) + { + for (int i = 0; i < zd.Length; i++) + { + nuint limb = zd[i]; + zd[i] = ((limb & 0xFFFFFFFF) << BitsPerUInt32) | (limb >> BitsPerUInt32); + } + } BigInteger result = new(zd, negative); @@ -3380,60 +3402,6 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r return result; } - /// - /// Extracts 32-bit words from nuint limbs using arithmetic operations (endianness-safe). - /// Word 0 is the least significant 32 bits of limb 0. - /// - private static void ExtractWords(ReadOnlySpan limbs, Span words) - { - if (Environment.Is64BitProcess) - { - int w = 0; - for (int i = 0; i < limbs.Length; i++) - { - words[w++] = (uint)limbs[i]; - if (w < words.Length) - { - words[w++] = (uint)(limbs[i] >> BitsPerUInt32); - } - } - } - else - { - for (int i = 0; i < words.Length; i++) - { - words[i] = (uint)limbs[i]; - } - } - } - - /// - /// Packs 32-bit words back into nuint limbs using arithmetic operations (endianness-safe). - /// - private static void PackWords(ReadOnlySpan words, Span limbs) - { - if (Environment.Is64BitProcess) - { - int w = 0; - for (int i = 0; i < limbs.Length; i++) - { - nuint limb = words[w++]; - if (w < words.Length) - { - limb |= (nuint)words[w++] << BitsPerUInt32; - } - limbs[i] = limb; - } - } - else - { - for (int i = 0; i < words.Length; i++) - { - limbs[i] = words[i]; - } - } - } - /// public static BigInteger TrailingZeroCount(BigInteger value) { From 7d1b36c8090dec004dc54aa95a001234e73ea8c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:39:01 +0000 Subject: [PATCH 13/20] Add SIMD (Vector128/256/512) optimizations to LeftShiftSelf32/RightShiftSelf32 Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/1b00027e-fb5c-4b5b-8fbe-3b028d4cb2ac Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../Numerics/BigIntegerCalculator.ShiftRot.cs | 120 ++++++++++++++++-- 1 file changed, 112 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index dea541dca164a7..7fe0a6a23c8c59 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -116,13 +116,65 @@ private static void LeftShiftSelf32(Span bits, int shift, out uint carry) } int back = 32 - shift; - carry = bits[^1] >> back; - for (int i = bits.Length - 1; i > 0; i--) + if (Vector128.IsHardwareAccelerated) + { + carry = bits[^1] >> back; + + ref uint start = ref MemoryMarshal.GetReference(bits); + int offset = bits.Length; + + while (Vector512.IsHardwareAccelerated && offset >= Vector512.Count + 1) + { + Vector512 current = Vector512.LoadUnsafe(ref start, (nuint)(offset - Vector512.Count)) << shift; + Vector512 carries = Vector512.LoadUnsafe(ref start, (nuint)(offset - (Vector512.Count + 1))) >> back; + + Vector512 newValue = current | carries; + + Vector512.StoreUnsafe(newValue, ref start, (nuint)(offset - Vector512.Count)); + offset -= Vector512.Count; + } + + while (Vector256.IsHardwareAccelerated && offset >= Vector256.Count + 1) + { + Vector256 current = Vector256.LoadUnsafe(ref start, (nuint)(offset - Vector256.Count)) << shift; + Vector256 carries = Vector256.LoadUnsafe(ref start, (nuint)(offset - (Vector256.Count + 1))) >> back; + + Vector256 newValue = current | carries; + + Vector256.StoreUnsafe(newValue, ref start, (nuint)(offset - Vector256.Count)); + offset -= Vector256.Count; + } + + while (Vector128.IsHardwareAccelerated && offset >= Vector128.Count + 1) + { + Vector128 current = Vector128.LoadUnsafe(ref start, (nuint)(offset - Vector128.Count)) << shift; + Vector128 carries = Vector128.LoadUnsafe(ref start, (nuint)(offset - (Vector128.Count + 1))) >> back; + + Vector128 newValue = current | carries; + + Vector128.StoreUnsafe(newValue, ref start, (nuint)(offset - Vector128.Count)); + offset -= Vector128.Count; + } + + uint carry2 = 0; + for (int i = 0; i < offset; i++) + { + uint value = carry2 | bits[i] << shift; + carry2 = bits[i] >> back; + bits[i] = value; + } + } + else { - bits[i] = (bits[i] << shift) | (bits[i - 1] >> back); + carry = 0; + for (int i = 0; i < bits.Length; i++) + { + uint value = carry | bits[i] << shift; + carry = bits[i] >> back; + bits[i] = value; + } } - bits[0] <<= shift; } private static void RightShiftSelf32(Span bits, int shift, out uint carry) @@ -136,13 +188,65 @@ private static void RightShiftSelf32(Span bits, int shift, out uint carry) } int back = 32 - shift; - carry = bits[0] << back; - for (int i = 0; i < bits.Length - 1; i++) + if (Vector128.IsHardwareAccelerated) { - bits[i] = (bits[i] >> shift) | (bits[i + 1] << back); + carry = bits[0] << back; + + ref uint start = ref MemoryMarshal.GetReference(bits); + int offset = 0; + + while (Vector512.IsHardwareAccelerated && bits.Length - offset >= Vector512.Count + 1) + { + Vector512 current = Vector512.LoadUnsafe(ref start, (nuint)offset) >> shift; + Vector512 carries = Vector512.LoadUnsafe(ref start, (nuint)(offset + 1)) << back; + + Vector512 newValue = current | carries; + + Vector512.StoreUnsafe(newValue, ref start, (nuint)offset); + offset += Vector512.Count; + } + + while (Vector256.IsHardwareAccelerated && bits.Length - offset >= Vector256.Count + 1) + { + Vector256 current = Vector256.LoadUnsafe(ref start, (nuint)offset) >> shift; + Vector256 carries = Vector256.LoadUnsafe(ref start, (nuint)(offset + 1)) << back; + + Vector256 newValue = current | carries; + + Vector256.StoreUnsafe(newValue, ref start, (nuint)offset); + offset += Vector256.Count; + } + + while (Vector128.IsHardwareAccelerated && bits.Length - offset >= Vector128.Count + 1) + { + Vector128 current = Vector128.LoadUnsafe(ref start, (nuint)offset) >> shift; + Vector128 carries = Vector128.LoadUnsafe(ref start, (nuint)(offset + 1)) << back; + + Vector128 newValue = current | carries; + + Vector128.StoreUnsafe(newValue, ref start, (nuint)offset); + offset += Vector128.Count; + } + + uint carry2 = 0; + for (int i = bits.Length - 1; i >= offset; i--) + { + uint value = carry2 | bits[i] >> shift; + carry2 = bits[i] << back; + bits[i] = value; + } + } + else + { + carry = 0; + for (int i = bits.Length - 1; i >= 0; i--) + { + uint value = carry | bits[i] >> shift; + carry = bits[i] << back; + bits[i] = value; + } } - bits[^1] >>= shift; } public static void LeftShiftSelf(Span bits, int shift, out nuint carry) From 05ce40b28546db47c4cb2f7dae5bd337a2fc9a45 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:28:54 +0000 Subject: [PATCH 14/20] Replace ArrayPool with RentedBuffer in SwapUpperAndLower32 Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8c281b87-a8a9-4270-b328-9a161aa1e7ab Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../src/System/Numerics/BigIntegerCalculator.ShiftRot.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index 7fe0a6a23c8c59..5b48be9baf4a89 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -86,8 +86,10 @@ private static void SwapUpperAndLower32(Span bits, int lowerLength) Span lowerDst = bits.Slice(upperLength); int tmpLength = Math.Min(lowerLength, upperLength); - uint[] tmpArray = System.Buffers.ArrayPool.Shared.Rent(tmpLength); - Span tmp = tmpArray.AsSpan(0, tmpLength); + int wordsPerLimb = nint.Size / sizeof(uint); + int nuintCount = (tmpLength + wordsPerLimb - 1) / wordsPerLimb; + Span tmpNuint = BigInteger.RentedBuffer.Create(nuintCount, out BigInteger.RentedBuffer tmpBuffer); + Span tmp = MemoryMarshal.Cast(tmpNuint).Slice(0, tmpLength); if (upperLength < lowerLength) { @@ -102,7 +104,7 @@ private static void SwapUpperAndLower32(Span bits, int lowerLength) tmp.CopyTo(lowerDst); } - System.Buffers.ArrayPool.Shared.Return(tmpArray); + tmpBuffer.Dispose(); } private static void LeftShiftSelf32(Span bits, int shift, out uint carry) From 2357508a726b52d067b70076d2cb919342afcb65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 02:00:03 +0000 Subject: [PATCH 15/20] Add mixed-bit negative multiword PopCount tests Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8f164a62-0ae0-43f4-a405-fb8f045c133a Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../tests/BigIntegerTests.GenericMath.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs index 32bce6c8648ad3..ec854f470d535f 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs @@ -308,6 +308,19 @@ public static void PopCountTest() // -(2^64): one's complement of upper limbs = all 1s, lowest limb = 0 → PopCount depends on limb count Assert.Equal((BigInteger)32, BigInteger.PopCount(-BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + // Mixed-bit negative multiword cases: exercises the PopCount formula + // PopCount(2^W - m) = W - PopCount(m) - TZC(m) + 1 with non-trivial magnitudePopCount and magnitudeTZC. + // -(2^32 + 7): magnitude [0x00000007, 0x00000001], W=64, PopCount(m)=4, TZC(m)=0 → 64-4-0+1 = 61 + Assert.Equal((BigInteger)61, BigInteger.PopCount(-(BigInteger.Pow(2, 32) + 7))); + // -(2^33 + 5): magnitude [0x00000005, 0x00000002], W=64, PopCount(m)=3, TZC(m)=0 → 64-3-0+1 = 62 + Assert.Equal((BigInteger)62, BigInteger.PopCount(-(BigInteger.Pow(2, 33) + 5))); + // -(2^64 + 3): magnitude [0x00000003, 0x00000000, 0x00000001], W=96, PopCount(m)=3, TZC(m)=0 → 96-3-0+1 = 94 + Assert.Equal((BigInteger)94, BigInteger.PopCount(-(BigInteger.Pow(2, 64) + 3))); + // -(2^64 + 4): magnitude with non-trivial TZC, W=96, PopCount(m)=2, TZC(m)=2 → 96-2-2+1 = 93 + Assert.Equal((BigInteger)93, BigInteger.PopCount(-(BigInteger.Pow(2, 64) + 4))); + // -(2^65 + 12): magnitude with mixed bits and TZC=2, W=96, PopCount(m)=3, TZC(m)=2 → 96-3-2+1 = 92 + Assert.Equal((BigInteger)92, BigInteger.PopCount(-(BigInteger.Pow(2, 65) + 12))); + // Results must be the same on 32-bit and 64-bit platforms. Assert.Equal((BigInteger)0, BigInteger.PopCount(BigInteger.Zero)); Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.One)); From 620c2d9a1b7ee3705b754da468c358a5d8af3449 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:17:47 +0000 Subject: [PATCH 16/20] Simplify PopCount negative path and deduplicate SIMD in LeftShiftSelf32/RightShiftSelf32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PopCount: Replace formula-based approach with direct nuint two's complement plus MSL upper-32-bit correction, using IndexOfAnyExcept for first non-zero. LeftShiftSelf32/RightShiftSelf32: Delegate to nuint LeftShiftSelf/RightShiftSelf (which have SIMD acceleration) on 64-bit LE and 32-bit, eliminating duplicated Vector128/256/512 code. Scalar fallback for big-endian 64-bit only. Tests: Fix Globalization.NumberStyles → NumberStyles in GenericMath tests. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/57602d8b-20c6-45f3-9fbe-5940e7ddfe8f Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 63 +++----- .../Numerics/BigIntegerCalculator.ShiftRot.cs | 141 +++++++----------- .../tests/BigIntegerTests.GenericMath.cs | 64 ++++---- 3 files changed, 108 insertions(+), 160 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 345e654f0b4e8d..b0e32bf701aaab 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3221,53 +3221,34 @@ public static BigInteger PopCount(BigInteger value) } else { - // When the value is negative, we compute PopCount of the two's complement - // representation using 32-bit word semantics. - // - // Using the identity: PopCount(2^W - m) = W - PopCount(m) - TZC(m) + 1 - // where W is the total width in terms of 32-bit words and m is the magnitude. - // - // This avoids platform-dependent results from complementing nuint limbs, - // since ~(nuint) fills upper bits with 1s on 64-bit when the magnitude - // only uses the lower 32 bits. - - ulong magnitudePopCount = 0; - ulong magnitudeTZC = 0; - bool foundNonZero = false; + // When the value is negative, we need to PopCount the two's complement + // representation. We'll do this "inline" to avoid needing to unnecessarily allocate. - for (int i = 0; i < value._bits.Length; i++) - { - nuint part = value._bits[i]; - magnitudePopCount += (ulong)BitOperations.PopCount(part); + int firstNonZero = value._bits.AsSpan().IndexOfAnyExcept((nuint)0); - if (!foundNonZero) - { - if (part == 0) - { - magnitudeTZC += (uint)BigIntegerCalculator.BitsPerLimb; - } - else - { - magnitudeTZC += (ulong)BitOperations.TrailingZeroCount(part); - foundNonZero = true; - } - } - } + int i = firstNonZero; + nuint part; - // Compute W: total width in terms of 32-bit words. - // On 64-bit, each nuint holds two 32-bit words, except the MSL's upper - // half is excluded when it's zero (matching the original uint[] layout). - int wordCount = value._bits.Length; - if (Environment.Is64BitProcess) + // Negate the first non-zero limb (two's complement start). + part = ~value._bits[i] + 1; + result += (ulong)BitOperations.PopCount(part); + i++; + + while (i < value._bits.Length) { - wordCount *= 2; - if ((uint)(value._bits[^1] >> BitsPerUInt32) == 0) - { - wordCount--; - } + // Then process the remaining limbs using ones' complement. + part = ~value._bits[i]; + result += (ulong)BitOperations.PopCount(part); + i++; } - result = (ulong)wordCount * BitsPerUInt32 - magnitudePopCount - magnitudeTZC + 1; + // On 64-bit, when the MSL's upper 32 bits are zero, complementing + // produces 0xFFFFFFFF in those bits, adding 32 phantom 1-bits. + // Subtract them to maintain 32-bit word semantics. + if (Environment.Is64BitProcess && (uint)(value._bits[^1] >> BitsPerUInt32) == 0) + { + result -= BitsPerUInt32; + } } return result; diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index 5b48be9baf4a89..16863a79f0bf88 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -118,62 +118,40 @@ private static void LeftShiftSelf32(Span bits, int shift, out uint carry) } int back = 32 - shift; + carry = bits[^1] >> back; - if (Vector128.IsHardwareAccelerated) + if (!Environment.Is64BitProcess) { - carry = bits[^1] >> back; - - ref uint start = ref MemoryMarshal.GetReference(bits); - int offset = bits.Length; - - while (Vector512.IsHardwareAccelerated && offset >= Vector512.Count + 1) - { - Vector512 current = Vector512.LoadUnsafe(ref start, (nuint)(offset - Vector512.Count)) << shift; - Vector512 carries = Vector512.LoadUnsafe(ref start, (nuint)(offset - (Vector512.Count + 1))) >> back; - - Vector512 newValue = current | carries; - - Vector512.StoreUnsafe(newValue, ref start, (nuint)(offset - Vector512.Count)); - offset -= Vector512.Count; - } - - while (Vector256.IsHardwareAccelerated && offset >= Vector256.Count + 1) - { - Vector256 current = Vector256.LoadUnsafe(ref start, (nuint)(offset - Vector256.Count)) << shift; - Vector256 carries = Vector256.LoadUnsafe(ref start, (nuint)(offset - (Vector256.Count + 1))) >> back; - - Vector256 newValue = current | carries; - - Vector256.StoreUnsafe(newValue, ref start, (nuint)(offset - Vector256.Count)); - offset -= Vector256.Count; - } + // On 32-bit, nuint and uint are the same size; delegate directly. + Span view = MemoryMarshal.Cast(bits); + LeftShiftSelf(view, shift, out _); + return; + } - while (Vector128.IsHardwareAccelerated && offset >= Vector128.Count + 1) + if (BitConverter.IsLittleEndian && bits.Length >= 2) + { + // On 64-bit LE, shifting a Span produces identical bit-level + // results to shifting the same memory as Span, because the + // carry propagation within each nuint matches the uint-to-uint carry. + int evenCount = bits.Length & ~1; + Span nuintView = MemoryMarshal.Cast(bits.Slice(0, evenCount)); + LeftShiftSelf(nuintView, shift, out nuint nuintCarry); + + if ((bits.Length & 1) != 0) { - Vector128 current = Vector128.LoadUnsafe(ref start, (nuint)(offset - Vector128.Count)) << shift; - Vector128 carries = Vector128.LoadUnsafe(ref start, (nuint)(offset - (Vector128.Count + 1))) >> back; - - Vector128 newValue = current | carries; - - Vector128.StoreUnsafe(newValue, ref start, (nuint)(offset - Vector128.Count)); - offset -= Vector128.Count; + bits[^1] = (bits[^1] << shift) | (uint)nuintCarry; } - uint carry2 = 0; - for (int i = 0; i < offset; i++) - { - uint value = carry2 | bits[i] << shift; - carry2 = bits[i] >> back; - bits[i] = value; - } + return; } - else + + // Scalar fallback for big-endian 64-bit. { - carry = 0; + uint c = 0; for (int i = 0; i < bits.Length; i++) { - uint value = carry | bits[i] << shift; - carry = bits[i] >> back; + uint value = c | bits[i] << shift; + c = bits[i] >> back; bits[i] = value; } } @@ -190,62 +168,51 @@ private static void RightShiftSelf32(Span bits, int shift, out uint carry) } int back = 32 - shift; + carry = bits[0] << back; - if (Vector128.IsHardwareAccelerated) + if (!Environment.Is64BitProcess) { - carry = bits[0] << back; - - ref uint start = ref MemoryMarshal.GetReference(bits); - int offset = 0; - - while (Vector512.IsHardwareAccelerated && bits.Length - offset >= Vector512.Count + 1) - { - Vector512 current = Vector512.LoadUnsafe(ref start, (nuint)offset) >> shift; - Vector512 carries = Vector512.LoadUnsafe(ref start, (nuint)(offset + 1)) << back; - - Vector512 newValue = current | carries; + // On 32-bit, nuint and uint are the same size; delegate directly. + Span view = MemoryMarshal.Cast(bits); + RightShiftSelf(view, shift, out _); + return; + } - Vector512.StoreUnsafe(newValue, ref start, (nuint)offset); - offset += Vector512.Count; - } + if (BitConverter.IsLittleEndian && bits.Length >= 2) + { + // On 64-bit LE, right-shifting Span produces identical bit-level + // results to right-shifting the same memory as Span. + int evenCount = bits.Length & ~1; - while (Vector256.IsHardwareAccelerated && bits.Length - offset >= Vector256.Count + 1) + if ((bits.Length & 1) != 0) { - Vector256 current = Vector256.LoadUnsafe(ref start, (nuint)offset) >> shift; - Vector256 carries = Vector256.LoadUnsafe(ref start, (nuint)(offset + 1)) << back; + // Handle the odd highest uint first, then shift the lower even portion. + uint carryDown = bits[^1] << back; + bits[^1] >>= shift; - Vector256 newValue = current | carries; + Span nuintView = MemoryMarshal.Cast(bits.Slice(0, evenCount)); + RightShiftSelf(nuintView, shift, out _); - Vector256.StoreUnsafe(newValue, ref start, (nuint)offset); - offset += Vector256.Count; + // Inject carry from the odd top element into the upper half of the + // highest nuint (which is bits[evenCount-1] on LE). + bits[evenCount - 1] |= carryDown; } - - while (Vector128.IsHardwareAccelerated && bits.Length - offset >= Vector128.Count + 1) + else { - Vector128 current = Vector128.LoadUnsafe(ref start, (nuint)offset) >> shift; - Vector128 carries = Vector128.LoadUnsafe(ref start, (nuint)(offset + 1)) << back; - - Vector128 newValue = current | carries; - - Vector128.StoreUnsafe(newValue, ref start, (nuint)offset); - offset += Vector128.Count; + Span nuintView = MemoryMarshal.Cast(bits); + RightShiftSelf(nuintView, shift, out _); } - uint carry2 = 0; - for (int i = bits.Length - 1; i >= offset; i--) - { - uint value = carry2 | bits[i] >> shift; - carry2 = bits[i] << back; - bits[i] = value; - } + return; } - else + + // Scalar fallback for big-endian 64-bit. { - carry = 0; + uint c = 0; for (int i = bits.Length - 1; i >= 0; i--) { - uint value = carry | bits[i] >> shift; - carry = bits[i] << back; + uint value = c | bits[i] >> shift; + c = bits[i] << back; bits[i] = value; } } diff --git a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs index c9f7beeee7cab6..224d786727a1c5 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigIntegerTests.GenericMath.cs @@ -270,28 +270,28 @@ public static void LeadingZeroCountTest() // Large positive values: LZC is the leading zero count of the most significant 32-bit word. // 2^31 (= int.MaxValue+1): MSW = 0x80000000, LZC = 0 - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("080000000", NumberStyles.HexNumber))); // uint.MaxValue (0xFFFFFFFF): MSW = 0xFFFFFFFF, LZC = 0 - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("0FFFFFFFF", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("0FFFFFFFF", NumberStyles.HexNumber))); // 2^32 (= uint.MaxValue+1): MSW = 0x00000001, LZC = 31 - Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("0100000000", NumberStyles.HexNumber))); // long.MaxValue (0x7FFFFFFFFFFFFFFF): MSW = 0x7FFFFFFF, LZC = 1 - Assert.Equal((BigInteger)1, BigInteger.LeadingZeroCount(BigInteger.Parse("07FFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)1, BigInteger.LeadingZeroCount(BigInteger.Parse("07FFFFFFFFFFFFFFF", NumberStyles.HexNumber))); // 2^63 (= long.MaxValue+1): MSW = 0x80000000, LZC = 0 - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("08000000000000000", NumberStyles.HexNumber))); // ulong.MaxValue (0xFFFFFFFFFFFFFFFF): MSW = 0xFFFFFFFF, LZC = 0 - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("0FFFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("0FFFFFFFFFFFFFFFF", NumberStyles.HexNumber))); // 2^64 (= ulong.MaxValue+1): MSW = 0x00000001, LZC = 31 - Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("010000000000000000", NumberStyles.HexNumber))); // 2^127: MSW = 0x80000000, LZC = 0 - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("080000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(BigInteger.Parse("080000000000000000000000000000000", NumberStyles.HexNumber))); // 2^128: MSW = 0x00000001, LZC = 31 - Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("0100000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)31, BigInteger.LeadingZeroCount(BigInteger.Parse("0100000000000000000000000000000000", NumberStyles.HexNumber))); // Large negative values always return 0. - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); - Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("080000000", NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("0100000000", NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.LeadingZeroCount(-BigInteger.Parse("08000000000000000", NumberStyles.HexNumber))); // Results must be the same on 32-bit and 64-bit platforms. Assert.Equal((BigInteger)32, BigInteger.LeadingZeroCount(BigInteger.Zero)); @@ -326,27 +326,27 @@ public static void PopCountTest() // Large positive values via _bits path. // 2^31 (0x80000000): one bit set - Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("080000000", NumberStyles.HexNumber))); // uint.MaxValue (0xFFFFFFFF): 32 bits set - Assert.Equal((BigInteger)32, BigInteger.PopCount(BigInteger.Parse("0FFFFFFFF", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)32, BigInteger.PopCount(BigInteger.Parse("0FFFFFFFF", NumberStyles.HexNumber))); // 2^32 (0x100000000): one bit set - Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("0100000000", NumberStyles.HexNumber))); // long.MaxValue (0x7FFFFFFFFFFFFFFF): 63 bits set - Assert.Equal((BigInteger)63, BigInteger.PopCount(BigInteger.Parse("07FFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)63, BigInteger.PopCount(BigInteger.Parse("07FFFFFFFFFFFFFFF", NumberStyles.HexNumber))); // ulong.MaxValue (0xFFFFFFFFFFFFFFFF): 64 bits set - Assert.Equal((BigInteger)64, BigInteger.PopCount(BigInteger.Parse("0FFFFFFFFFFFFFFFF", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)64, BigInteger.PopCount(BigInteger.Parse("0FFFFFFFFFFFFFFFF", NumberStyles.HexNumber))); // 2^64 (0x10000000000000000): one bit set - Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("010000000000000000", NumberStyles.HexNumber))); // 2^128: one bit set - Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("0100000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(BigInteger.Parse("0100000000000000000000000000000000", NumberStyles.HexNumber))); // Large negative values via _bits path (two's complement). // -(2^31): two's complement of 0x80000000 within 32 bits = 0x80000000 → PopCount = 1 - Assert.Equal((BigInteger)1, BigInteger.PopCount(-BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)1, BigInteger.PopCount(-BigInteger.Parse("080000000", NumberStyles.HexNumber))); // -(2^32): two's complement of [0x00000000, 0x00000001] = [0x00000000, 0xFFFFFFFF] → PopCount = 32 - Assert.Equal((BigInteger)32, BigInteger.PopCount(-BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)32, BigInteger.PopCount(-BigInteger.Parse("0100000000", NumberStyles.HexNumber))); // -(2^64): one's complement of upper limbs = all 1s, lowest limb = 0 → PopCount depends on limb count - Assert.Equal((BigInteger)32, BigInteger.PopCount(-BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)32, BigInteger.PopCount(-BigInteger.Parse("010000000000000000", NumberStyles.HexNumber))); // Mixed-bit negative multiword cases: exercises the PopCount formula // PopCount(2^W - m) = W - PopCount(m) - TZC(m) + 1 with non-trivial magnitudePopCount and magnitudeTZC. @@ -547,23 +547,23 @@ public static void TrailingZeroCountTest() // Large positive values via _bits path. // 2^31 (0x80000000): 31 trailing zeros - Assert.Equal((BigInteger)31, BigInteger.TrailingZeroCount(BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)31, BigInteger.TrailingZeroCount(BigInteger.Parse("080000000", NumberStyles.HexNumber))); // uint.MaxValue (0xFFFFFFFF): 0 trailing zeros - Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(BigInteger.Parse("0FFFFFFFF", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)0, BigInteger.TrailingZeroCount(BigInteger.Parse("0FFFFFFFF", NumberStyles.HexNumber))); // 2^32 (0x100000000): 32 trailing zeros - Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(BigInteger.Parse("0100000000", NumberStyles.HexNumber))); // 2^63 (0x8000000000000000): 63 trailing zeros - Assert.Equal((BigInteger)63, BigInteger.TrailingZeroCount(BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)63, BigInteger.TrailingZeroCount(BigInteger.Parse("08000000000000000", NumberStyles.HexNumber))); // 2^64 (0x10000000000000000): 64 trailing zeros - Assert.Equal((BigInteger)64, BigInteger.TrailingZeroCount(BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)64, BigInteger.TrailingZeroCount(BigInteger.Parse("010000000000000000", NumberStyles.HexNumber))); // 2^128: 128 trailing zeros - Assert.Equal((BigInteger)128, BigInteger.TrailingZeroCount(BigInteger.Parse("0100000000000000000000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)128, BigInteger.TrailingZeroCount(BigInteger.Parse("0100000000000000000000000000000000", NumberStyles.HexNumber))); // Large negative values via _bits path (two's complement shares trailing zeros with magnitude). - Assert.Equal((BigInteger)31, BigInteger.TrailingZeroCount(-BigInteger.Parse("080000000", Globalization.NumberStyles.HexNumber))); - Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(-BigInteger.Parse("0100000000", Globalization.NumberStyles.HexNumber))); - Assert.Equal((BigInteger)63, BigInteger.TrailingZeroCount(-BigInteger.Parse("08000000000000000", Globalization.NumberStyles.HexNumber))); - Assert.Equal((BigInteger)64, BigInteger.TrailingZeroCount(-BigInteger.Parse("010000000000000000", Globalization.NumberStyles.HexNumber))); + Assert.Equal((BigInteger)31, BigInteger.TrailingZeroCount(-BigInteger.Parse("080000000", NumberStyles.HexNumber))); + Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(-BigInteger.Parse("0100000000", NumberStyles.HexNumber))); + Assert.Equal((BigInteger)63, BigInteger.TrailingZeroCount(-BigInteger.Parse("08000000000000000", NumberStyles.HexNumber))); + Assert.Equal((BigInteger)64, BigInteger.TrailingZeroCount(-BigInteger.Parse("010000000000000000", NumberStyles.HexNumber))); // Results must be the same on 32-bit and 64-bit platforms. Assert.Equal((BigInteger)32, BigInteger.TrailingZeroCount(BigInteger.Zero)); From c0e1c28bde09eb5ef8c2f391e5438790f5358519 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:25:43 +0000 Subject: [PATCH 17/20] Rename rotation helpers: remove "32" suffix, improve documentation Remove the "32" suffix from RotateLeft32, RotateRight, SwapUpperAndLower32, LeftShiftSelf32, and RightShiftSelf32 since the nuint-based versions no longer exist. Add clearer documentation explaining that the rotation operates on a Span view of the nuint[] buffer, with shift operations delegating to the SIMD-accelerated nuint overloads while carry extraction and digit swapping operate at 32-bit word granularity. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/f2568909-19c7-4a74-9b63-a849eee3dce8 Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 2 +- .../Numerics/BigIntegerCalculator.ShiftRot.cs | 45 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index b0e32bf701aaab..87fa028020224c 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3374,7 +3374,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r Span zwSpan = allWords.Slice(0, zWordCount); - BigIntegerCalculator.RotateLeft32(zwSpan, rotateLeftAmount); + BigIntegerCalculator.RotateLeft(zwSpan, rotateLeftAmount); if (negative && (int)zwSpan[^1] < 0) { diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index 16863a79f0bf88..fd57040c865f13 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -11,9 +11,18 @@ internal static partial class BigIntegerCalculator { /// /// Rotates a span of 32-bit words left by the specified amount. - /// This provides platform-independent 32-bit word rotation semantics. /// - public static void RotateLeft32(Span bits, long rotateLeftAmount) + /// + /// The caller provides a of words obtained via + /// from the underlying nuint[] buffer. + /// On 64-bit, the last nuint limb may contain 32 extra zero bits compared to the + /// uint[] storage; the caller trims those before passing the word span. The shift + /// operations delegate to the SIMD-accelerated LeftShiftSelf/RightShiftSelf + /// nuint overloads internally, while carry extraction and digit swapping operate at + /// 32-bit word granularity (the swap point may fall mid-nuint when the word count + /// is odd on 64-bit). + /// + public static void RotateLeft(Span bits, long rotateLeftAmount) { Debug.Assert(Math.Abs(rotateLeftAmount) <= 0x80000000); @@ -30,7 +39,7 @@ public static void RotateLeft32(Span bits, long rotateLeftAmount) (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerWord); } - RotateRight32(bits, digitShift % bits.Length, smallShift); + RotateRight(bits, digitShift % bits.Length, smallShift); } else { @@ -39,15 +48,15 @@ public static void RotateLeft32(Span bits, long rotateLeftAmount) (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerWord); } - RotateLeft32(bits, digitShift % bits.Length, smallShift); + RotateLeft(bits, digitShift % bits.Length, smallShift); } } - private static void RotateLeft32(Span bits, int digitShift, int smallShift) + private static void RotateLeft(Span bits, int digitShift, int smallShift) { Debug.Assert(bits.Length > 0); - LeftShiftSelf32(bits, smallShift, out uint carry); + LeftShiftSelf(bits, smallShift, out uint carry); bits[0] |= carry; if (digitShift == 0) @@ -55,14 +64,14 @@ private static void RotateLeft32(Span bits, int digitShift, int smallShift return; } - SwapUpperAndLower32(bits, bits.Length - digitShift); + SwapUpperAndLower(bits, bits.Length - digitShift); } - private static void RotateRight32(Span bits, int digitShift, int smallShift) + private static void RotateRight(Span bits, int digitShift, int smallShift) { Debug.Assert(bits.Length > 0); - RightShiftSelf32(bits, smallShift, out uint carry); + RightShiftSelf(bits, smallShift, out uint carry); bits[^1] |= carry; if (digitShift == 0) @@ -70,10 +79,10 @@ private static void RotateRight32(Span bits, int digitShift, int smallShif return; } - SwapUpperAndLower32(bits, digitShift); + SwapUpperAndLower(bits, digitShift); } - private static void SwapUpperAndLower32(Span bits, int lowerLength) + private static void SwapUpperAndLower(Span bits, int lowerLength) { Debug.Assert(lowerLength > 0); Debug.Assert(lowerLength < bits.Length); @@ -107,7 +116,12 @@ private static void SwapUpperAndLower32(Span bits, int lowerLength) tmpBuffer.Dispose(); } - private static void LeftShiftSelf32(Span bits, int shift, out uint carry) + /// + /// Left-shifts a span of 32-bit words, returning the bits that shifted out of the top word. + /// Delegates to the SIMD-accelerated LeftShiftSelf(Span<nuint>, ...) + /// overload where possible; the carry is always extracted at 32-bit word granularity. + /// + private static void LeftShiftSelf(Span bits, int shift, out uint carry) { Debug.Assert((uint)shift < 32); @@ -157,7 +171,12 @@ private static void LeftShiftSelf32(Span bits, int shift, out uint carry) } } - private static void RightShiftSelf32(Span bits, int shift, out uint carry) + /// + /// Right-shifts a span of 32-bit words, returning the bits that shifted out of the bottom word. + /// Delegates to the SIMD-accelerated RightShiftSelf(Span<nuint>, ...) + /// overload where possible; the carry is always extracted at 32-bit word granularity. + /// + private static void RightShiftSelf(Span bits, int shift, out uint carry) { Debug.Assert((uint)shift < 32); From c876239c4c415440d48d480d1c74a1daef754b37 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:15:46 +0000 Subject: [PATCH 18/20] Restructure rotation: restore nuint RotateLeft in BigIntegerCalculator, 32-bit uses nuint path, 64-bit uses uint view with inline rotation Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/fc887d25-0f15-446b-b265-1ff699c595ee Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 127 +++++++++++++++--- .../Numerics/BigIntegerCalculator.ShiftRot.cs | 72 ++++++---- 2 files changed, 154 insertions(+), 45 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 87fa028020224c..a4373641590425 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3301,36 +3301,37 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r Debug.Assert(bits.Length > 0); Debug.Assert(Math.Abs(rotateLeftAmount) <= 0x80000000); - // Determine the number of 32-bit words in the magnitude. - // On 64-bit, each nuint limb holds two 32-bit words; the MSL's upper - // half may be zero (matching the original uint[] layout). - int wordCount; - if (Environment.Is64BitProcess) - { - wordCount = bits.Length * 2; - if ((uint)(bits[^1] >> BitsPerUInt32) == 0) - { - wordCount--; - } + if (!Environment.Is64BitProcess) + { + // On 32-bit, nuint and uint are the same width so the standard nuint + // rotation algorithm (with BitsPerLimb = 32) is directly correct. + return RotateNuint(bits, negative, rotateLeftAmount); } - else + + // On 64-bit, each nuint limb holds two 32-bit words. The rotation ring + // width must be a multiple of 32 bits (not 64) for platform-independent + // results, and sign-extension for negative values adds one 32-bit word + // (not one 64-bit limb). Both of these requirements mean the ring width + // may not align to nuint boundaries, so we work at 32-bit word granularity + // via MemoryMarshal.Cast. + + int wordCount = bits.Length * 2; + if ((uint)(bits[^1] >> BitsPerUInt32) == 0) { - wordCount = bits.Length; + wordCount--; } - // Allocate the result buffer with one extra word for possible sign-extension, - // and work directly in it to avoid a temporary array. + // Allocate one extra word for possible sign-extension. int maxWordCount = wordCount + 1; - int zLimbCount = Environment.Is64BitProcess ? (maxWordCount + 1) / 2 : maxWordCount; + int zLimbCount = (maxWordCount + 1) / 2; Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); - // Copy input magnitude and zero any extra limbs for sign extension / partial last limb. bits.CopyTo(zd); zd.Slice(bits.Length).Clear(); // On big-endian 64-bit, swap uint halves within each limb so that // MemoryMarshal.Cast yields words in low-to-high order. - if (Environment.Is64BitProcess && !BitConverter.IsLittleEndian) + if (!BitConverter.IsLittleEndian) { for (int i = 0; i < zd.Length; i++) { @@ -3372,9 +3373,47 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r } } + // Rotate at 32-bit word granularity. Span zwSpan = allWords.Slice(0, zWordCount); + { + const int BitsPerWord = 32; + int digitShiftMax = (int)(0x80000000 / BitsPerWord); + int digitShift = digitShiftMax; + int smallShift = 0; + + if (rotateLeftAmount < 0) + { + if (rotateLeftAmount != -0x80000000) + { + (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerWord); + } + + BigIntegerCalculator.RightShiftSelf(zwSpan, smallShift, out uint carry); + zwSpan[^1] |= carry; + + digitShift %= zwSpan.Length; + if (digitShift != 0) + { + BigIntegerCalculator.SwapUpperAndLower(zwSpan, digitShift); + } + } + else + { + if (rotateLeftAmount != 0x80000000) + { + (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerWord); + } - BigIntegerCalculator.RotateLeft(zwSpan, rotateLeftAmount); + BigIntegerCalculator.LeftShiftSelf(zwSpan, smallShift, out uint carry); + zwSpan[0] |= carry; + + digitShift %= zwSpan.Length; + if (digitShift != 0) + { + BigIntegerCalculator.SwapUpperAndLower(zwSpan, zwSpan.Length - digitShift); + } + } + } if (negative && (int)zwSpan[^1] < 0) { @@ -3396,7 +3435,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r } // On big-endian 64-bit, swap uint halves back to restore correct nuint layout. - if (Environment.Is64BitProcess && !BitConverter.IsLittleEndian) + if (!BitConverter.IsLittleEndian) { for (int i = 0; i < zd.Length; i++) { @@ -3412,6 +3451,54 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r return result; } + /// + /// Rotation using the standard nuint algorithm. Only correct on 32-bit where + /// nuint and uint have the same width (BitsPerLimb = 32). + /// + private static BigInteger RotateNuint(ReadOnlySpan bits, bool negative, long rotateLeftAmount) + { + Debug.Assert(!Environment.Is64BitProcess); + + int zLength = bits.Length; + int leadingZeroCount = negative ? bits.IndexOfAnyExcept((nuint)0) : 0; + + if (negative && (nint)bits[^1] < 0 + && (leadingZeroCount != bits.Length - 1 || bits[^1] != ((nuint)1 << (BigIntegerCalculator.BitsPerLimb - 1)))) + { + ++zLength; + } + + Span zd = RentedBuffer.Create(zLength, out RentedBuffer zdBuffer); + + zd[^1] = 0; + bits.CopyTo(zd); + + if (negative) + { + Debug.Assert((uint)leadingZeroCount < (uint)zd.Length); + + zd[leadingZeroCount] = (nuint)(-(nint)zd[leadingZeroCount]); + NumericsHelpers.DangerousMakeOnesComplement(zd.Slice(leadingZeroCount + 1)); + } + + BigIntegerCalculator.RotateLeft(zd, rotateLeftAmount); + + if (negative && (nint)zd[^1] < 0) + { + NumericsHelpers.DangerousMakeTwosComplement(zd); + } + else + { + negative = false; + } + + BigInteger result = new(zd, negative); + + zdBuffer.Dispose(); + + return result; + } + /// public static BigInteger TrailingZeroCount(BigInteger value) { diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index fd57040c865f13..b0fd9123587f8e 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -9,25 +9,11 @@ namespace System.Numerics { internal static partial class BigIntegerCalculator { - /// - /// Rotates a span of 32-bit words left by the specified amount. - /// - /// - /// The caller provides a of words obtained via - /// from the underlying nuint[] buffer. - /// On 64-bit, the last nuint limb may contain 32 extra zero bits compared to the - /// uint[] storage; the caller trims those before passing the word span. The shift - /// operations delegate to the SIMD-accelerated LeftShiftSelf/RightShiftSelf - /// nuint overloads internally, while carry extraction and digit swapping operate at - /// 32-bit word granularity (the swap point may fall mid-nuint when the word count - /// is odd on 64-bit). - /// - public static void RotateLeft(Span bits, long rotateLeftAmount) + public static void RotateLeft(Span bits, long rotateLeftAmount) { Debug.Assert(Math.Abs(rotateLeftAmount) <= 0x80000000); - const int BitsPerWord = 32; - int digitShiftMax = (int)(0x80000000 / BitsPerWord); + int digitShiftMax = (int)(0x80000000 / BitsPerLimb); int digitShift = digitShiftMax; int smallShift = 0; @@ -36,7 +22,7 @@ public static void RotateLeft(Span bits, long rotateLeftAmount) { if (rotateLeftAmount != -0x80000000) { - (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerWord); + (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerLimb); } RotateRight(bits, digitShift % bits.Length, smallShift); @@ -45,18 +31,18 @@ public static void RotateLeft(Span bits, long rotateLeftAmount) { if (rotateLeftAmount != 0x80000000) { - (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerWord); + (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerLimb); } RotateLeft(bits, digitShift % bits.Length, smallShift); } } - private static void RotateLeft(Span bits, int digitShift, int smallShift) + public static void RotateLeft(Span bits, int digitShift, int smallShift) { Debug.Assert(bits.Length > 0); - LeftShiftSelf(bits, smallShift, out uint carry); + LeftShiftSelf(bits, smallShift, out nuint carry); bits[0] |= carry; if (digitShift == 0) @@ -67,11 +53,11 @@ private static void RotateLeft(Span bits, int digitShift, int smallShift) SwapUpperAndLower(bits, bits.Length - digitShift); } - private static void RotateRight(Span bits, int digitShift, int smallShift) + public static void RotateRight(Span bits, int digitShift, int smallShift) { Debug.Assert(bits.Length > 0); - RightShiftSelf(bits, smallShift, out uint carry); + RightShiftSelf(bits, smallShift, out nuint carry); bits[^1] |= carry; if (digitShift == 0) @@ -82,7 +68,43 @@ private static void RotateRight(Span bits, int digitShift, int smallShift) SwapUpperAndLower(bits, digitShift); } - private static void SwapUpperAndLower(Span bits, int lowerLength) + private static void SwapUpperAndLower(Span bits, int lowerLength) + { + Debug.Assert(lowerLength > 0); + Debug.Assert(lowerLength < bits.Length); + + int upperLength = bits.Length - lowerLength; + + Span lower = bits.Slice(0, lowerLength); + Span upper = bits.Slice(lowerLength); + + Span lowerDst = bits.Slice(upperLength); + + int tmpLength = Math.Min(lowerLength, upperLength); + Span tmp = BigInteger.RentedBuffer.Create(tmpLength, out BigInteger.RentedBuffer tmpBuffer); + + if (upperLength < lowerLength) + { + upper.CopyTo(tmp); + lower.CopyTo(lowerDst); + tmp.CopyTo(bits); + } + else + { + lower.CopyTo(tmp); + upper.CopyTo(bits); + tmp.CopyTo(lowerDst); + } + + tmpBuffer.Dispose(); + } + + // 32-bit word helpers for the partial-limb edge case on 64-bit. + // When the last nuint limb has only 32 significant bits, the rotation ring + // width is not a multiple of BitsPerLimb. Shift and swap must then operate + // at 32-bit word granularity because the swap point may fall mid-nuint. + + public static void SwapUpperAndLower(Span bits, int lowerLength) { Debug.Assert(lowerLength > 0); Debug.Assert(lowerLength < bits.Length); @@ -121,7 +143,7 @@ private static void SwapUpperAndLower(Span bits, int lowerLength) /// Delegates to the SIMD-accelerated LeftShiftSelf(Span<nuint>, ...) /// overload where possible; the carry is always extracted at 32-bit word granularity. /// - private static void LeftShiftSelf(Span bits, int shift, out uint carry) + public static void LeftShiftSelf(Span bits, int shift, out uint carry) { Debug.Assert((uint)shift < 32); @@ -176,7 +198,7 @@ private static void LeftShiftSelf(Span bits, int shift, out uint carry) /// Delegates to the SIMD-accelerated RightShiftSelf(Span<nuint>, ...) /// overload where possible; the carry is always extracted at 32-bit word granularity. /// - private static void RightShiftSelf(Span bits, int shift, out uint carry) + public static void RightShiftSelf(Span bits, int shift, out uint carry) { Debug.Assert((uint)shift < 32); From cf79f6ca539d05582abc65ce73c38bae2e1927f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:18:25 +0000 Subject: [PATCH 19/20] Address code review: use BitsPerUInt32 instead of inline constant, make nuint RotateLeft/Right overloads private Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/fc887d25-0f15-446b-b265-1ff699c595ee Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 7 +++---- .../src/System/Numerics/BigIntegerCalculator.ShiftRot.cs | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index a4373641590425..8fc99a9035ab7f 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3376,8 +3376,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r // Rotate at 32-bit word granularity. Span zwSpan = allWords.Slice(0, zWordCount); { - const int BitsPerWord = 32; - int digitShiftMax = (int)(0x80000000 / BitsPerWord); + int digitShiftMax = (int)(0x80000000 / BitsPerUInt32); int digitShift = digitShiftMax; int smallShift = 0; @@ -3385,7 +3384,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r { if (rotateLeftAmount != -0x80000000) { - (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerWord); + (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerUInt32); } BigIntegerCalculator.RightShiftSelf(zwSpan, smallShift, out uint carry); @@ -3401,7 +3400,7 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r { if (rotateLeftAmount != 0x80000000) { - (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerWord); + (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerUInt32); } BigIntegerCalculator.LeftShiftSelf(zwSpan, smallShift, out uint carry); diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index b0fd9123587f8e..5634e868f860f2 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -38,7 +38,7 @@ public static void RotateLeft(Span bits, long rotateLeftAmount) } } - public static void RotateLeft(Span bits, int digitShift, int smallShift) + private static void RotateLeft(Span bits, int digitShift, int smallShift) { Debug.Assert(bits.Length > 0); @@ -53,7 +53,7 @@ public static void RotateLeft(Span bits, int digitShift, int smallShift) SwapUpperAndLower(bits, bits.Length - digitShift); } - public static void RotateRight(Span bits, int digitShift, int smallShift) + private static void RotateRight(Span bits, int digitShift, int smallShift) { Debug.Assert(bits.Length > 0); From 1ee9f9f8e6d66c9df8d6c82bbbdad09dd22e9552 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 22:49:36 +0000 Subject: [PATCH 20/20] Rework rotation to use nuint directly with last-index fixup per reviewer feedback On 64-bit with even word count: standard nuint rotation, absorbing odd 32-bit digit shifts into the nuint small shift (0..63). On 64-bit with odd word count (half-used last limb): nuint SIMD shift with carry fixup, uint-level digit swap only where the swap boundary may fall mid-nuint. Two's complement conversion uses NumericsHelpers.DangerousMakeTwosComplement at nuint level with half-limb clearing. Removed LeftShiftSelf(Span) and RightShiftSelf(Span) since the bit-shift now operates at nuint level for all cases. Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/e72bd9b4-df13-46c5-8180-72d9cc2022f2 Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com> --- .../src/System/Numerics/BigInteger.cs | 224 +++++++++++------- .../Numerics/BigIntegerCalculator.ShiftRot.cs | 130 +--------- 2 files changed, 143 insertions(+), 211 deletions(-) diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 8fc99a9035ab7f..6e0e17fb475656 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -3308,124 +3308,174 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r return RotateNuint(bits, negative, rotateLeftAmount); } - // On 64-bit, each nuint limb holds two 32-bit words. The rotation ring - // width must be a multiple of 32 bits (not 64) for platform-independent - // results, and sign-extension for negative values adds one 32-bit word - // (not one 64-bit limb). Both of these requirements mean the ring width - // may not align to nuint boundaries, so we work at 32-bit word granularity - // via MemoryMarshal.Cast. + // On 64-bit, each nuint limb is 64 bits, but the rotation ring width must + // be a multiple of 32 bits for platform-independent results. The last limb + // may hold only one significant 32-bit word (upper 32 bits zero). + // Count effective 32-bit words. int wordCount = bits.Length * 2; - if ((uint)(bits[^1] >> BitsPerUInt32) == 0) + bool halfLimb = (uint)(bits[^1] >> BitsPerUInt32) == 0; + if (halfLimb) wordCount--; + + // Determine if sign extension adds a 32-bit word. + int zWordCount = wordCount; + int firstNonZeroLimb = negative ? bits.IndexOfAnyExcept((nuint)0) : 0; + + if (negative) { - wordCount--; + // The MSW's sign bit indicates whether two's complement needs an extra word. + bool mswSignBitSet = halfLimb + ? (int)(uint)bits[^1] < 0 // bit 31 of lower half + : (nint)bits[^1] < 0; // bit 63 (= MSW bit 31) + + if (mswSignBitSet) + { + // Sign extension needed unless value is exactly -2^(wordCount*32-1). + bool isMinValue = halfLimb + ? ((uint)bits[^1] == UInt32HighBit && firstNonZeroLimb == bits.Length - 1) + : (bits[^1] == ((nuint)UInt32HighBit << BitsPerUInt32) && firstNonZeroLimb == bits.Length - 1); + + if (!isMinValue) + ++zWordCount; + } } - // Allocate one extra word for possible sign-extension. - int maxWordCount = wordCount + 1; - int zLimbCount = (maxWordCount + 1) / 2; - Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); + // Allocate result buffer sized for zWordCount 32-bit words. + int zLimbCount = (zWordCount + 1) / 2; + bool resultHalfLimb = (zWordCount & 1) != 0; - bits.CopyTo(zd); + Span zd = RentedBuffer.Create(zLimbCount, out RentedBuffer zdBuffer); zd.Slice(bits.Length).Clear(); + bits.CopyTo(zd); - // On big-endian 64-bit, swap uint halves within each limb so that - // MemoryMarshal.Cast yields words in low-to-high order. - if (!BitConverter.IsLittleEndian) + // Two's complement conversion at nuint level. + if (negative) { - for (int i = 0; i < zd.Length; i++) + NumericsHelpers.DangerousMakeTwosComplement(zd); + + if (resultHalfLimb) { - nuint limb = zd[i]; - zd[i] = ((limb & 0xFFFFFFFF) << BitsPerUInt32) | (limb >> BitsPerUInt32); + // Complementing the zero padding in the upper 32 of the last limb + // produces phantom 0xFFFFFFFF; clear it. + zd[^1] = (nuint)(uint)zd[^1]; } } - // Get a Span working view directly into the nuint buffer. - Span allWords = MemoryMarshal.Cast(zd); - - int zWordCount = wordCount; + // Decompose the rotation amount at 32-bit word granularity. + int digitShift32 = (int)(0x80000000 / BitsPerUInt32); + int smallShift32 = 0; + bool rotateRight; - // For negative values, find the index of the first non-zero word. - Span wordsSpan = allWords.Slice(0, wordCount); - int leadingZeroCount = negative ? wordsSpan.IndexOfAnyExcept(0u) : 0; - - if (negative && (int)allWords[zWordCount - 1] < 0 - && (leadingZeroCount != zWordCount - 1 || allWords[zWordCount - 1] != UInt32HighBit)) + if (rotateLeftAmount < 0) { - // For a shift of N x 32 bit, - // We check for a special case where its sign bit could be outside the uint array after 2's complement conversion. - // For example given [0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF], its 2's complement is [0x01, 0x00, 0x00] - // After a 32 bit right shift, it becomes [0x00, 0x00] which is [0x00, 0x00] when converted back. - // The expected result is [0x00, 0x00, 0xFFFFFFFF] (2's complement) or [0x00, 0x00, 0x01] when converted back - // If the 2's complement's last element is a 0, we will track the sign externally - ++zWordCount; + rotateRight = true; + if (rotateLeftAmount != -0x80000000) + (digitShift32, smallShift32) = Math.DivRem(-(int)rotateLeftAmount, BitsPerUInt32); + } + else + { + rotateRight = false; + if (rotateLeftAmount != 0x80000000) + (digitShift32, smallShift32) = Math.DivRem((int)rotateLeftAmount, BitsPerUInt32); } - if (negative) + // Perform the rotation. + if (!resultHalfLimb) { - Debug.Assert((uint)leadingZeroCount < (uint)zWordCount); + // Even word count: the ring fills all nuint limbs completely. + // An odd 32-bit digit shift is absorbed into the nuint small shift (0..63). + int nuintSmallShift = (digitShift32 & 1) * BitsPerUInt32 + smallShift32; + int nuintDigitShift = digitShift32 >> 1; - // Two's complement conversion on the 32-bit word view. - allWords[leadingZeroCount] = (uint)(-(int)allWords[leadingZeroCount]); - for (int i = leadingZeroCount + 1; i < zWordCount; i++) + if (rotateRight) { - allWords[i] = ~allWords[i]; + BigIntegerCalculator.RightShiftSelf(zd, nuintSmallShift, out nuint carry); + zd[^1] |= carry; + + nuintDigitShift %= zd.Length; + if (nuintDigitShift != 0) + BigIntegerCalculator.SwapUpperAndLower(zd, nuintDigitShift); } - } + else + { + BigIntegerCalculator.LeftShiftSelf(zd, nuintSmallShift, out nuint carry); + zd[0] |= carry; - // Rotate at 32-bit word granularity. - Span zwSpan = allWords.Slice(0, zWordCount); + nuintDigitShift %= zd.Length; + if (nuintDigitShift != 0) + BigIntegerCalculator.SwapUpperAndLower(zd, zd.Length - nuintDigitShift); + } + } + else { - int digitShiftMax = (int)(0x80000000 / BitsPerUInt32); - int digitShift = digitShiftMax; - int smallShift = 0; - - if (rotateLeftAmount < 0) + // Odd word count: the last limb's upper 32 bits are not part of the ring. + // The SIMD-accelerated nuint shift handles the bit-level rotation; a carry + // fixup accounts for the half-used last limb. The digit swap operates at + // uint granularity via MemoryMarshal.Cast because the swap boundary may + // fall mid-nuint. + if (rotateRight) { - if (rotateLeftAmount != -0x80000000) + if (smallShift32 != 0) { - (digitShift, smallShift) = Math.DivRem(-(int)rotateLeftAmount, BitsPerUInt32); + BigIntegerCalculator.RightShiftSelf(zd, smallShift32, out nuint carry); + // The nuint carry is at bit positions (64-shift)..63. + // For the half-limb ring, it wraps to bit (32-shift)..31 of the last word. + zd[^1] |= carry >> BitsPerUInt32; } - BigIntegerCalculator.RightShiftSelf(zwSpan, smallShift, out uint carry); - zwSpan[^1] |= carry; - - digitShift %= zwSpan.Length; - if (digitShift != 0) + int effectiveDigitShift = digitShift32 % zWordCount; + if (effectiveDigitShift != 0) { - BigIntegerCalculator.SwapUpperAndLower(zwSpan, digitShift); + if (!BitConverter.IsLittleEndian) + SwapHalvesWithinLimbs(zd); + + Span words = MemoryMarshal.Cast(zd).Slice(0, zWordCount); + BigIntegerCalculator.SwapUpperAndLower(words, effectiveDigitShift); + + if (!BitConverter.IsLittleEndian) + SwapHalvesWithinLimbs(zd); } } else { - if (rotateLeftAmount != 0x80000000) + if (smallShift32 != 0) { - (digitShift, smallShift) = Math.DivRem((int)rotateLeftAmount, BitsPerUInt32); + BigIntegerCalculator.LeftShiftSelf(zd, smallShift32, out _); + // Bits that overflowed into the upper 32 of the last limb should wrap. + // The nuint carry is 0 since the upper 32 were zero and shift < 32. + nuint overflow = zd[^1] >> BitsPerUInt32; + zd[^1] = (nuint)(uint)zd[^1]; + zd[0] |= overflow; } - BigIntegerCalculator.LeftShiftSelf(zwSpan, smallShift, out uint carry); - zwSpan[0] |= carry; - - digitShift %= zwSpan.Length; - if (digitShift != 0) + int effectiveDigitShift = digitShift32 % zWordCount; + if (effectiveDigitShift != 0) { - BigIntegerCalculator.SwapUpperAndLower(zwSpan, zwSpan.Length - digitShift); + if (!BitConverter.IsLittleEndian) + SwapHalvesWithinLimbs(zd); + + Span words = MemoryMarshal.Cast(zd).Slice(0, zWordCount); + BigIntegerCalculator.SwapUpperAndLower(words, zWordCount - effectiveDigitShift); + + if (!BitConverter.IsLittleEndian) + SwapHalvesWithinLimbs(zd); } } } - if (negative && (int)zwSpan[^1] < 0) + // Check sign bit and convert back from two's complement if needed. + bool resultNeg = resultHalfLimb + ? negative && (int)(uint)zd[^1] < 0 + : negative && (nint)zd[^1] < 0; + + if (resultNeg) { - // Convert back from two's complement on the 32-bit word view. - int firstNonZero = zwSpan.IndexOfAnyExcept(0u); + NumericsHelpers.DangerousMakeTwosComplement(zd); - if ((uint)firstNonZero < (uint)zWordCount) + if (resultHalfLimb) { - allWords[firstNonZero] = (uint)(-(int)allWords[firstNonZero]); - for (int j = firstNonZero + 1; j < zWordCount; j++) - { - allWords[j] = ~allWords[j]; - } + // Clear phantom bits from complementing the zero padding. + zd[^1] = (nuint)(uint)zd[^1]; } } else @@ -3433,16 +3483,6 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r negative = false; } - // On big-endian 64-bit, swap uint halves back to restore correct nuint layout. - if (!BitConverter.IsLittleEndian) - { - for (int i = 0; i < zd.Length; i++) - { - nuint limb = zd[i]; - zd[i] = ((limb & 0xFFFFFFFF) << BitsPerUInt32) | (limb >> BitsPerUInt32); - } - } - BigInteger result = new(zd, negative); zdBuffer.Dispose(); @@ -3450,6 +3490,20 @@ private static BigInteger Rotate(ReadOnlySpan bits, bool negative, long r return result; } + /// + /// Swaps the upper and lower 32-bit halves within each nuint limb. + /// Used on big-endian 64-bit before/after MemoryMarshal.Cast<nuint, uint> + /// to ensure correct 32-bit word ordering. + /// + private static void SwapHalvesWithinLimbs(Span limbs) + { + for (int i = 0; i < limbs.Length; i++) + { + nuint v = limbs[i]; + limbs[i] = ((v & 0xFFFFFFFF) << BitsPerUInt32) | (v >> BitsPerUInt32); + } + } + /// /// Rotation using the standard nuint algorithm. Only correct on 32-bit where /// nuint and uint have the same width (BitsPerLimb = 32). diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs index 5634e868f860f2..907a5a87e69076 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigIntegerCalculator.ShiftRot.cs @@ -68,7 +68,7 @@ private static void RotateRight(Span bits, int digitShift, int smallShift SwapUpperAndLower(bits, digitShift); } - private static void SwapUpperAndLower(Span bits, int lowerLength) + public static void SwapUpperAndLower(Span bits, int lowerLength) { Debug.Assert(lowerLength > 0); Debug.Assert(lowerLength < bits.Length); @@ -99,10 +99,9 @@ private static void SwapUpperAndLower(Span bits, int lowerLength) tmpBuffer.Dispose(); } - // 32-bit word helpers for the partial-limb edge case on 64-bit. - // When the last nuint limb has only 32 significant bits, the rotation ring - // width is not a multiple of BitsPerLimb. Shift and swap must then operate - // at 32-bit word granularity because the swap point may fall mid-nuint. + // 32-bit word digit-swap for the partial-limb edge case on 64-bit. + // When the rotation ring has an odd number of 32-bit words, the digit-swap + // boundary may fall mid-nuint, so the swap must operate at uint granularity. public static void SwapUpperAndLower(Span bits, int lowerLength) { @@ -138,127 +137,6 @@ public static void SwapUpperAndLower(Span bits, int lowerLength) tmpBuffer.Dispose(); } - /// - /// Left-shifts a span of 32-bit words, returning the bits that shifted out of the top word. - /// Delegates to the SIMD-accelerated LeftShiftSelf(Span<nuint>, ...) - /// overload where possible; the carry is always extracted at 32-bit word granularity. - /// - public static void LeftShiftSelf(Span bits, int shift, out uint carry) - { - Debug.Assert((uint)shift < 32); - - carry = 0; - if (shift == 0 || bits.IsEmpty) - { - return; - } - - int back = 32 - shift; - carry = bits[^1] >> back; - - if (!Environment.Is64BitProcess) - { - // On 32-bit, nuint and uint are the same size; delegate directly. - Span view = MemoryMarshal.Cast(bits); - LeftShiftSelf(view, shift, out _); - return; - } - - if (BitConverter.IsLittleEndian && bits.Length >= 2) - { - // On 64-bit LE, shifting a Span produces identical bit-level - // results to shifting the same memory as Span, because the - // carry propagation within each nuint matches the uint-to-uint carry. - int evenCount = bits.Length & ~1; - Span nuintView = MemoryMarshal.Cast(bits.Slice(0, evenCount)); - LeftShiftSelf(nuintView, shift, out nuint nuintCarry); - - if ((bits.Length & 1) != 0) - { - bits[^1] = (bits[^1] << shift) | (uint)nuintCarry; - } - - return; - } - - // Scalar fallback for big-endian 64-bit. - { - uint c = 0; - for (int i = 0; i < bits.Length; i++) - { - uint value = c | bits[i] << shift; - c = bits[i] >> back; - bits[i] = value; - } - } - } - - /// - /// Right-shifts a span of 32-bit words, returning the bits that shifted out of the bottom word. - /// Delegates to the SIMD-accelerated RightShiftSelf(Span<nuint>, ...) - /// overload where possible; the carry is always extracted at 32-bit word granularity. - /// - public static void RightShiftSelf(Span bits, int shift, out uint carry) - { - Debug.Assert((uint)shift < 32); - - carry = 0; - if (shift == 0 || bits.IsEmpty) - { - return; - } - - int back = 32 - shift; - carry = bits[0] << back; - - if (!Environment.Is64BitProcess) - { - // On 32-bit, nuint and uint are the same size; delegate directly. - Span view = MemoryMarshal.Cast(bits); - RightShiftSelf(view, shift, out _); - return; - } - - if (BitConverter.IsLittleEndian && bits.Length >= 2) - { - // On 64-bit LE, right-shifting Span produces identical bit-level - // results to right-shifting the same memory as Span. - int evenCount = bits.Length & ~1; - - if ((bits.Length & 1) != 0) - { - // Handle the odd highest uint first, then shift the lower even portion. - uint carryDown = bits[^1] << back; - bits[^1] >>= shift; - - Span nuintView = MemoryMarshal.Cast(bits.Slice(0, evenCount)); - RightShiftSelf(nuintView, shift, out _); - - // Inject carry from the odd top element into the upper half of the - // highest nuint (which is bits[evenCount-1] on LE). - bits[evenCount - 1] |= carryDown; - } - else - { - Span nuintView = MemoryMarshal.Cast(bits); - RightShiftSelf(nuintView, shift, out _); - } - - return; - } - - // Scalar fallback for big-endian 64-bit. - { - uint c = 0; - for (int i = bits.Length - 1; i >= 0; i--) - { - uint value = c | bits[i] >> shift; - c = bits[i] << back; - bits[i] = value; - } - } - } - public static void LeftShiftSelf(Span bits, int shift, out nuint carry) { Debug.Assert((uint)shift < BitsPerLimb);