diff --git a/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj b/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj index b5142e8ca5377..dfc3acc553d17 100644 --- a/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj +++ b/src/libraries/System.Runtime.Numerics/src/System.Runtime.Numerics.csproj @@ -40,6 +40,7 @@ + diff --git a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs index c43eccf81f1a6..0b3c7dd6b8929 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Number.BigInteger.cs @@ -275,6 +275,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -330,12 +331,12 @@ internal static unsafe ParsingStatus TryParseBigInteger(ReadOnlySpan value if ((style & NumberStyles.AllowHexSpecifier) != 0) { - return TryParseBigIntegerHexNumberStyle(value, style, out result); + return TryParseBigIntegerHexOrBinaryNumberStyle, char>(value, style, out result); } if ((style & NumberStyles.AllowBinarySpecifier) != 0) { - return TryParseBigIntegerBinaryNumberStyle(value, style, out result); + return TryParseBigIntegerHexOrBinaryNumberStyle, char>(value, style, out result); } return TryParseBigIntegerNumber(value, style, info, out result); @@ -401,16 +402,18 @@ internal static BigInteger ParseBigInteger(ReadOnlySpan value, NumberStyle return result; } - internal static ParsingStatus TryParseBigIntegerHexNumberStyle(ReadOnlySpan value, NumberStyles style, out BigInteger result) + internal static ParsingStatus TryParseBigIntegerHexOrBinaryNumberStyle(ReadOnlySpan value, NumberStyles style, out BigInteger result) + where TParser : struct, IBigIntegerHexOrBinaryParser + where TChar : unmanaged, IBinaryInteger { - int whiteIndex = 0; + int whiteIndex; // Skip past any whitespace at the beginning. if ((style & NumberStyles.AllowLeadingWhite) != 0) { for (whiteIndex = 0; whiteIndex < value.Length; whiteIndex++) { - if (!IsWhite(value[whiteIndex])) + if (!IsWhite(uint.CreateTruncating(value[whiteIndex]))) break; } @@ -422,7 +425,7 @@ internal static ParsingStatus TryParseBigIntegerHexNumberStyle(ReadOnlySpan= 0; whiteIndex--) { - if (!IsWhite(value[whiteIndex])) + if (!IsWhite(uint.CreateTruncating(value[whiteIndex]))) break; } @@ -434,220 +437,111 @@ internal static ParsingStatus TryParseBigIntegerHexNumberStyle(ReadOnlySpan= 8; - uint partialValue = (isNegative && partialDigitCount > 0) ? 0xFFFFFFFFu : 0; - - uint[]? arrayFromPool = null; - - Span bitsBuffer = ((uint)blockCount <= BigIntegerCalculator.StackAllocThreshold - ? stackalloc uint[BigIntegerCalculator.StackAllocThreshold] - : arrayFromPool = ArrayPool.Shared.Rent(blockCount)).Slice(0, blockCount); - - int bitsBufferPos = blockCount - 1; - - try - { - for (int i = 0; i < value.Length; i++) + if (!TParser.TryParseUnalignedBlock(value[0..leadingBitsCount], out leading)) { - char digitChar = value[i]; - - if (!HexConverter.IsHexChar(digitChar)) goto FailExit; - int hexValue = HexConverter.FromChar(digitChar); - - partialValue = (partialValue << 4) | (uint)hexValue; - partialDigitCount++; - - if (partialDigitCount == DigitsPerBlock) - { - bitsBuffer[bitsBufferPos] = partialValue; - bitsBufferPos--; - partialValue = 0; - partialDigitCount = 0; - } + goto FailExit; } - Debug.Assert(partialDigitCount == 0 && bitsBufferPos == -1); + // Fill leading sign bits + leading |= signBits << (leadingBitsCount * TParser.BitsPerDigit); + value = value[leadingBitsCount..]; + } - if (isNegative) + // Skip all the blocks consists of the same bit of sign + while (!value.IsEmpty && leading == signBits) + { + if (!TParser.TryParseSingleBlock(value[0..TParser.DigitsPerBlock], out leading)) { - NumericsHelpers.DangerousMakeTwosComplement(bitsBuffer); + goto FailExit; } + value = value[TParser.DigitsPerBlock..]; + } - // BigInteger requires leading zero blocks to be truncated. - bitsBuffer = bitsBuffer.TrimEnd(0u); - - int sign; - uint[]? bits; - - if (bitsBuffer.IsEmpty) + if (value.IsEmpty) + { + // There's nothing beyond significant leading block. Return it as the result. + if ((int)(leading ^ signBits) >= 0) { - sign = 0; - bits = null; + // Small value that fits in Int32. + // Delegate to the constructor for int.MinValue handling. + result = new BigInteger((int)leading); + return ParsingStatus.OK; } - else if (bitsBuffer.Length == 1 && bitsBuffer[0] <= int.MaxValue) + else if (leading != 0) { - sign = (int)bitsBuffer[0] * (isNegative ? -1 : 1); - bits = null; + // The sign of result differs with leading digit. + // Require to store in _bits. + + // Positive: sign=1, bits=[leading] + // Negative: sign=-1, bits=[leading ^ -1 + 1]=[-leading] + result = new BigInteger((int)signBits | 1, [leading ^ signBits - signBits]); + return ParsingStatus.OK; } else { - sign = isNegative ? -1 : 1; - bits = bitsBuffer.ToArray(); - } - - result = new BigInteger(sign, bits); - return ParsingStatus.OK; - } - finally - { - if (arrayFromPool != null) - { - ArrayPool.Shared.Return(arrayFromPool); + // -1 << 32, which requires an additional uint + result = new BigInteger(-1, [0, 1]); + return ParsingStatus.OK; } } - FailExit: - result = default; - return ParsingStatus.Failed; - } - - internal static ParsingStatus TryParseBigIntegerBinaryNumberStyle(ReadOnlySpan value, NumberStyles style, out BigInteger result) - { - int whiteIndex = 0; + // Now the size of bits array can be calculated, except edge cases of -2^32N + int wholeBlockCount = value.Length / TParser.DigitsPerBlock; + int totalUIntCount = wholeBlockCount + 1; - // Skip past any whitespace at the beginning. - if ((style & NumberStyles.AllowLeadingWhite) != 0) + // Early out for too large input + if (totalUIntCount > BigInteger.MaxLength) { - for (whiteIndex = 0; whiteIndex < value.Length; whiteIndex++) - { - if (!IsWhite(value[whiteIndex])) - break; - } - - value = value[whiteIndex..]; + result = default; + return ParsingStatus.Overflow; } - // Skip past any whitespace at the end. - if ((style & NumberStyles.AllowTrailingWhite) != 0) - { - for (whiteIndex = value.Length - 1; whiteIndex >= 0; whiteIndex--) - { - if (!IsWhite(value[whiteIndex])) - break; - } - - value = value[..(whiteIndex + 1)]; - } + uint[] bits = new uint[totalUIntCount]; + Span wholeBlockDestination = bits.AsSpan(0, wholeBlockCount); - if (value.IsEmpty) + if (!TParser.TryParseWholeBlocks(value, wholeBlockDestination)) { goto FailExit; } - int totalDigitCount = value.Length; - int partialDigitCount; + bits[^1] = leading; - (int blockCount, int remainder) = int.DivRem(totalDigitCount, BigInteger.kcbitUint); - if (remainder == 0) + if (signBits != 0) { - partialDigitCount = 0; - } - else - { - blockCount++; - partialDigitCount = BigInteger.kcbitUint - remainder; - } - - if (value[0] is not ('0' or '1')) goto FailExit; - bool isNegative = value[0] == '1'; - uint currentBlock = isNegative ? 0xFF_FF_FF_FFu : 0x0; - - uint[]? arrayFromPool = null; - Span buffer = ((uint)blockCount <= BigIntegerCalculator.StackAllocThreshold - ? stackalloc uint[BigIntegerCalculator.StackAllocThreshold] - : arrayFromPool = ArrayPool.Shared.Rent(blockCount)).Slice(0, blockCount); - - int bufferPos = blockCount - 1; - - try - { - for (int i = 0; i < value.Length; i++) + // For negative values, negate the whole array + if (bits.AsSpan().ContainsAnyExcept(0u)) { - char digitChar = value[i]; - - if (digitChar is not ('0' or '1')) goto FailExit; - currentBlock = (currentBlock << 1) | (uint)(digitChar - '0'); - partialDigitCount++; - - if (partialDigitCount == BigInteger.kcbitUint) - { - buffer[bufferPos--] = currentBlock; - partialDigitCount = 0; - - // we do not need to reset currentBlock now, because it should always set all its bits by left shift in subsequent iterations - } - } - - Debug.Assert(partialDigitCount == 0 && bufferPos == -1); - - buffer = buffer.TrimEnd(0u); - - int sign; - uint[]? bits; - - if (buffer.IsEmpty) - { - sign = 0; - bits = null; - } - else if (buffer.Length == 1) - { - sign = (int)buffer[0]; - bits = null; - - if ((!isNegative && sign < 0) || sign == int.MinValue) - { - bits = new[] { (uint)sign }; - sign = isNegative ? -1 : 1; - } + NumericsHelpers.DangerousMakeTwosComplement(bits); } else { - sign = isNegative ? -1 : 1; - bits = buffer.ToArray(); - - if (isNegative) - { - NumericsHelpers.DangerousMakeTwosComplement(bits); - } + // For negative values with all-zero trailing digits, + // It requires additional leading 1. + bits = new uint[bits.Length + 1]; + bits[^1] = 1; } - result = new BigInteger(sign, bits); + result = new BigInteger(-1, bits); return ParsingStatus.OK; } - finally + else { - if (arrayFromPool is not null) - { - ArrayPool.Shared.Return(arrayFromPool); - } + Debug.Assert(leading != 0); + + // For positive values, it's done + result = new BigInteger(1, bits); + return ParsingStatus.OK; } FailExit: @@ -1452,4 +1346,97 @@ internal static bool TryFormatBigInteger(BigInteger value, ReadOnlySpan fo } } } + + internal interface IBigIntegerHexOrBinaryParser + where TParser : struct, IBigIntegerHexOrBinaryParser + where TChar : unmanaged, IBinaryInteger + { + static abstract int BitsPerDigit { get; } + + static virtual int DigitsPerBlock => sizeof(uint) * 8 / TParser.BitsPerDigit; + + static abstract NumberStyles BlockNumberStyle { get; } + + static abstract uint GetSignBitsIfValid(uint ch); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static virtual bool TryParseUnalignedBlock(ReadOnlySpan input, out uint result) + { + if (typeof(TChar) == typeof(char)) + { + return uint.TryParse(MemoryMarshal.Cast(input), TParser.BlockNumberStyle, null, out result); + } + + throw new NotSupportedException(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static virtual bool TryParseSingleBlock(ReadOnlySpan input, out uint result) + => TParser.TryParseUnalignedBlock(input, out result); + + static virtual bool TryParseWholeBlocks(ReadOnlySpan input, Span destination) + { + Debug.Assert(destination.Length * TParser.DigitsPerBlock == input.Length); + ref TChar lastWholeBlockStart = ref Unsafe.Add(ref MemoryMarshal.GetReference(input), input.Length - TParser.DigitsPerBlock); + + for (int i = 0; i < destination.Length; i++) + { + if (!TParser.TryParseSingleBlock( + MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Subtract(ref lastWholeBlockStart, i * TParser.DigitsPerBlock), TParser.DigitsPerBlock), + out destination[i])) + { + return false; + } + } + + return true; + } + } + + internal readonly struct BigIntegerHexParser : IBigIntegerHexOrBinaryParser, TChar> + where TChar : unmanaged, IBinaryInteger + { + public static int BitsPerDigit => 4; + + public static NumberStyles BlockNumberStyle => NumberStyles.AllowHexSpecifier; + + // A valid ASCII hex digit is positive (0-7) if it starts with 00110 + public static uint GetSignBitsIfValid(uint ch) => (uint)((ch & 0b_1111_1000) == 0b_0011_0000 ? 0 : -1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseWholeBlocks(ReadOnlySpan input, Span destination) + { + if (typeof(TChar) == typeof(char)) + { + if (Convert.FromHexString(MemoryMarshal.Cast(input), MemoryMarshal.AsBytes(destination), out _, out _) != OperationStatus.Done) + { + return false; + } + + if (BitConverter.IsLittleEndian) + { + MemoryMarshal.AsBytes(destination).Reverse(); + } + else + { + destination.Reverse(); + } + + return true; + } + + throw new NotSupportedException(); + } + } + + internal readonly struct BigIntegerBinaryParser : IBigIntegerHexOrBinaryParser, TChar> + where TChar : unmanaged, IBinaryInteger + { + public static int BitsPerDigit => 1; + + public static NumberStyles BlockNumberStyle => NumberStyles.AllowBinarySpecifier; + + // Taking the LSB is enough for distinguishing 0/1 + public static uint GetSignBitsIfValid(uint ch) => (uint)(((int)ch << 31) >> 31); + } } diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/NumericsHelpers.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/NumericsHelpers.cs index 797b848b496d5..6190f14433e33 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/NumericsHelpers.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/NumericsHelpers.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; namespace System.Numerics { @@ -97,22 +99,57 @@ public static double GetDoubleFromParts(int sign, int exp, ulong man) // a mutation and needs to be used with care for immutable types. public static void DangerousMakeTwosComplement(Span d) { + // Given a number: + // XXXXXXXXXXXY00000 + // where Y is non-zero, + // The result of two's complement is + // AAAAAAAAAAAB00000 + // where A = ~X and B = -Y + + // Trim trailing 0s (at the first in little endian array) + d = d.TrimStart(0u); + + // Make the first non-zero element to be two's complement if (d.Length > 0) { - d[0] = unchecked(~d[0] + 1); + d[0] = (uint)(-(int)d[0]); + d = d.Slice(1); + } - int i = 1; + if (d.IsEmpty) + { + return; + } - // first do complement and +1 as long as carry is needed - for (; d[i - 1] == 0 && i < d.Length; i++) - { - d[i] = unchecked(~d[i] + 1); - } - // now ones complement is sufficient - for (; i < d.Length; i++) - { - d[i] = ~d[i]; - } + // Make one's complement for other elements + int offset = 0; + + ref uint start = ref MemoryMarshal.GetReference(d); + + while (Vector512.IsHardwareAccelerated && d.Length - offset >= Vector512.Count) + { + Vector512 complement = ~Vector512.LoadUnsafe(ref start, (nuint)offset); + Vector512.StoreUnsafe(complement, ref start, (nuint)offset); + offset += Vector512.Count; + } + + while (Vector256.IsHardwareAccelerated && d.Length - offset >= Vector256.Count) + { + Vector256 complement = ~Vector256.LoadUnsafe(ref start, (nuint)offset); + Vector256.StoreUnsafe(complement, ref start, (nuint)offset); + offset += Vector256.Count; + } + + while (Vector128.IsHardwareAccelerated && d.Length - offset >= Vector128.Count) + { + Vector128 complement = ~Vector128.LoadUnsafe(ref start, (nuint)offset); + Vector128.StoreUnsafe(complement, ref start, (nuint)offset); + offset += Vector128.Count; + } + + for (; offset < d.Length; offset++) + { + d[offset] = ~d[offset]; } } diff --git a/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs b/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs index 508efc963f9d1..86652ef6550a8 100644 --- a/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs +++ b/src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs @@ -148,6 +148,16 @@ public void Parse_Hex32Bits() Assert.Equal(new BigInteger(-2), result); Assert.Equal(-2, result); + Assert.True(BigInteger.TryParse("F", NumberStyles.HexNumber, null, out result)); + Assert.Equal(-1, result); + + for (int i = 0; i < 40; i++) + { + string test = "F" + new string('0', i); + Assert.True(BigInteger.TryParse(test, NumberStyles.HexNumber, null, out result)); + Assert.Equal(BigInteger.MinusOne << (4 * i), result); + } + Assert.Throws(() => { BigInteger.Parse("zzz", NumberStyles.HexNumber); @@ -159,6 +169,19 @@ public void Parse_Hex32Bits() }); } + [Theory] + [InlineData("1", -1L)] + [InlineData("01", 1L)] + [InlineData("10000000000000000000000000000000", (long)int.MinValue)] + [InlineData("010000000000000000000000000000001", 0x080000001L)] + [InlineData("111111111111111111111111111111110", -2L)] + [InlineData("0111111111111111111111111111111111", 0x1FFFFFFFFL)] + public void Parse_BinSpecialCases(string input, long expectedValue) + { + Assert.True(BigInteger.TryParse(input, NumberStyles.BinaryNumber, null, out BigInteger result)); + Assert.Equal(expectedValue, result); + } + public static IEnumerable RegressionIssueRuntime94610_TestData() { yield return new object[] @@ -466,6 +489,12 @@ private static void VerifyBinaryNumberStyles(NumberStyles ns, Random random) { VerifyParseToString(GetBinaryDigitSequence(1, 100, random) + GetRandomInvalidChar(random) + GetBinaryDigitSequence(1, 10, random), ns, false); } + + // Power of 2 + for (int i = 0; i < 70; i++) + { + VerifyParseToString("1" + new string('0', i), ns, true); + } } private static void VerifyNumberStyles(NumberStyles ns, Random random)