From ccf86dbdd3f2981becabeb931d998420da2f7d4b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Oct 2025 17:43:26 +0000
Subject: [PATCH 1/9] Initial plan
From 0ab3dfe64d04415770742ed9c19262ebb62a673b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Oct 2025 18:14:59 +0000
Subject: [PATCH 2/9] Add HexFloat NumberStyles and update validation logic
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../System/Globalization/NumberFormatInfo.cs | 24 +++++++++++++++----
.../src/System/Globalization/NumberStyles.cs | 3 +++
2 files changed, 22 insertions(+), 5 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs
index 7e41139b4bf6c5..a2c9b04a74de75 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs
@@ -827,13 +827,27 @@ static void ThrowInvalid(NumberStyles value)
internal static void ValidateParseStyleFloatingPoint(NumberStyles style)
{
- // Check for undefined flags or hex number
- if ((style & (InvalidNumberStyles | NumberStyles.AllowHexSpecifier | NumberStyles.AllowBinarySpecifier)) != 0)
+ // Check for undefined flags
+ if ((style & InvalidNumberStyles) != 0)
{
- ThrowInvalid(style);
+ throw new ArgumentException(SR.Argument_InvalidNumberStyles, nameof(style));
+ }
+
+ // Binary specifier is not supported for floating point
+ if ((style & NumberStyles.AllowBinarySpecifier) != 0)
+ {
+ throw new ArgumentException(SR.Arg_HexBinaryStylesNotSupported, nameof(style));
+ }
- static void ThrowInvalid(NumberStyles value) =>
- throw new ArgumentException((value & InvalidNumberStyles) != 0 ? SR.Argument_InvalidNumberStyles : SR.Arg_HexBinaryStylesNotSupported, nameof(style));
+ // When AllowHexSpecifier is used, only specific flags are allowed
+ if ((style & NumberStyles.AllowHexSpecifier) != 0)
+ {
+ // HexFloat allows: AllowLeadingWhite, AllowTrailingWhite, AllowLeadingSign, AllowHexSpecifier, AllowDecimalPoint, AllowExponent
+ NumberStyles invalidFlags = style & ~NumberStyles.HexFloat;
+ if (invalidFlags != 0)
+ {
+ throw new ArgumentException(SR.Arg_InvalidHexBinaryStyle, nameof(style));
+ }
}
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs
index 84a8afdf39a3a2..0a34af0de92036 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberStyles.cs
@@ -73,6 +73,9 @@ public enum NumberStyles
Float = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign |
AllowDecimalPoint | AllowExponent,
+ /// Indicates that the , , , , , and styles are used for hexadecimal floating-point values. This is a composite number style.
+ HexFloat = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowHexSpecifier | AllowDecimalPoint | AllowExponent,
+
Currency = AllowLeadingWhite | AllowTrailingWhite | AllowLeadingSign | AllowTrailingSign |
AllowParentheses | AllowDecimalPoint | AllowThousands | AllowCurrencySymbol,
From 163d95ac99003f260fffe31a0ba77f8dae628ce2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Oct 2025 18:31:32 +0000
Subject: [PATCH 3/9] Implement hex float parsing logic
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../src/System/Number.Parsing.cs | 318 ++++++++++++++++++
.../System.Runtime/ref/System.Runtime.cs | 1 +
2 files changed, 319 insertions(+)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs
index 3748792c1784d3..5c74498aa33fca 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs
@@ -979,10 +979,328 @@ internal static bool SpanEqualsOrdinalIgnoreCase(ReadOnlySpan span
}
}
+ private static bool TryParseHexFloat(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out TFloat result)
+ where TChar : unmanaged, IUtfChar
+ where TFloat : unmanaged, IBinaryFloatParseAndFormatInfo
+ {
+ result = TFloat.Zero;
+
+ if (value.IsEmpty)
+ {
+ return false;
+ }
+
+ int index = 0;
+
+ // Skip leading whitespace
+ if ((styles & NumberStyles.AllowLeadingWhite) != 0)
+ {
+ while (index < value.Length && IsWhite(TChar.CastToUInt32(value[index])))
+ {
+ index++;
+ }
+ }
+
+ if (index >= value.Length)
+ {
+ return false;
+ }
+
+ // Parse optional sign
+ bool isNegative = false;
+ if ((styles & NumberStyles.AllowLeadingSign) != 0)
+ {
+ ReadOnlySpan positiveSign = info.PositiveSignTChar();
+ ReadOnlySpan negativeSign = info.NegativeSignTChar();
+
+ if (value.Slice(index).StartsWith(negativeSign))
+ {
+ isNegative = true;
+ index += negativeSign.Length;
+ }
+ else if (value.Slice(index).StartsWith(positiveSign))
+ {
+ index += positiveSign.Length;
+ }
+ }
+
+ if (index >= value.Length)
+ {
+ return false;
+ }
+
+ // Parse "0x" or "0X" prefix
+ if (index + 1 >= value.Length ||
+ TChar.CastToUInt32(value[index]) != '0' ||
+ (TChar.CastToUInt32(value[index + 1]) != 'x' && TChar.CastToUInt32(value[index + 1]) != 'X'))
+ {
+ return false;
+ }
+ index += 2;
+
+ if (index >= value.Length)
+ {
+ return false;
+ }
+
+ // Parse hex significand (mantissa)
+ ulong integerPart = 0;
+ int integerDigits = 0;
+ bool hasIntegerPart = false;
+
+ // Parse integer part before decimal point
+ while (index < value.Length && HexConverter.IsHexChar((int)TChar.CastToUInt32(value[index])))
+ {
+ hasIntegerPart = true;
+ if (integerDigits < 16) // Avoid overflow, we only need the significant bits
+ {
+ integerPart = (integerPart << 4) | (uint)HexConverter.FromChar((int)TChar.CastToUInt32(value[index]));
+ integerDigits++;
+ }
+ index++;
+ }
+
+ // Parse fractional part after decimal point
+ ulong fractionalPart = 0;
+ int fractionalDigits = 0;
+ bool hasFractionalPart = false;
+
+ if ((styles & NumberStyles.AllowDecimalPoint) != 0 &&
+ index < value.Length &&
+ TChar.CastToUInt32(value[index]) == '.')
+ {
+ index++;
+
+ while (index < value.Length && HexConverter.IsHexChar((int)TChar.CastToUInt32(value[index])))
+ {
+ hasFractionalPart = true;
+ if (fractionalDigits < 16)
+ {
+ fractionalPart = (fractionalPart << 4) | (uint)HexConverter.FromChar((int)TChar.CastToUInt32(value[index]));
+ fractionalDigits++;
+ }
+ index++;
+ }
+ }
+
+ // Must have at least one hex digit
+ if (!hasIntegerPart && !hasFractionalPart)
+ {
+ return false;
+ }
+
+ // Parse binary exponent (p or P)
+ if ((styles & NumberStyles.AllowExponent) == 0 || index >= value.Length ||
+ (TChar.CastToUInt32(value[index]) != 'p' && TChar.CastToUInt32(value[index]) != 'P'))
+ {
+ return false;
+ }
+ index++;
+
+ if (index >= value.Length)
+ {
+ return false;
+ }
+
+ // Parse exponent sign
+ bool exponentIsNegative = false;
+ if (TChar.CastToUInt32(value[index]) == '-')
+ {
+ exponentIsNegative = true;
+ index++;
+ }
+ else if (TChar.CastToUInt32(value[index]) == '+')
+ {
+ index++;
+ }
+
+ if (index >= value.Length)
+ {
+ return false;
+ }
+
+ // Parse exponent value (decimal digits)
+ int exponent = 0;
+ bool hasExponentDigits = false;
+ while (index < value.Length && IsDigit(TChar.CastToUInt32(value[index])))
+ {
+ hasExponentDigits = true;
+ int digit = (int)(TChar.CastToUInt32(value[index]) - '0');
+
+ // Prevent overflow
+ if (exponent <= (int.MaxValue - digit) / 10)
+ {
+ exponent = exponent * 10 + digit;
+ }
+ else
+ {
+ exponent = int.MaxValue;
+ }
+ index++;
+ }
+
+ if (!hasExponentDigits)
+ {
+ return false;
+ }
+
+ if (exponentIsNegative)
+ {
+ exponent = -exponent;
+ }
+
+ // Skip trailing whitespace
+ if ((styles & NumberStyles.AllowTrailingWhite) != 0)
+ {
+ while (index < value.Length && IsWhite(TChar.CastToUInt32(value[index])))
+ {
+ index++;
+ }
+ }
+
+ // Must have consumed entire string
+ if (index != value.Length)
+ {
+ return false;
+ }
+
+ // Convert to floating point
+ result = HexFloatToFloat(integerPart, fractionalPart, fractionalDigits, exponent, isNegative);
+ return true;
+ }
+
+ private static TFloat HexFloatToFloat(ulong integerPart, ulong fractionalPart, int fractionalDigits, int binaryExponent, bool isNegative)
+ where TFloat : unmanaged, IBinaryFloatParseAndFormatInfo
+ {
+ // Handle zero
+ if (integerPart == 0 && fractionalPart == 0)
+ {
+ return isNegative ? -TFloat.Zero : TFloat.Zero;
+ }
+
+ // Combine integer and fractional parts into a single significand
+ // The hex significand represents: integerPart + fractionalPart * 16^(-fractionalDigits)
+ // We need to normalize this and adjust the exponent
+
+ ulong significand;
+
+ if (integerPart != 0)
+ {
+ // Normalize integer part
+ int shift = 64 - BitOperations.LeadingZeroCount(integerPart);
+ significand = integerPart;
+
+ // Add fractional part if space allows
+ if (fractionalDigits > 0 && shift < 64)
+ {
+ int fractionalShift = fractionalDigits * 4;
+ if (fractionalShift < 64 - shift)
+ {
+ significand = (significand << fractionalShift) | fractionalPart;
+ shift += fractionalShift;
+ }
+ else
+ {
+ // Partial fractional part
+ int availableShift = 64 - shift;
+ int usedFractionalDigits = availableShift / 4;
+ significand = (significand << (usedFractionalDigits * 4)) | (fractionalPart >> ((fractionalDigits - usedFractionalDigits) * 4));
+ shift = 64;
+ }
+ }
+
+ binaryExponent += shift - (fractionalDigits * 4);
+ }
+ else
+ {
+ // Only fractional part
+ int shift = 64 - BitOperations.LeadingZeroCount(fractionalPart);
+ significand = fractionalPart << (64 - shift);
+ binaryExponent += -((fractionalDigits * 4) - (64 - shift));
+ }
+
+ // Convert to IEEE 754 representation
+ int mantissaBits = TFloat.NormalMantissaBits;
+ int exponentBias = TFloat.ExponentBias;
+
+ // Normalize significand to have the MSB set
+ if (significand == 0)
+ {
+ return isNegative ? -TFloat.Zero : TFloat.Zero;
+ }
+
+ int leadingZeros = BitOperations.LeadingZeroCount(significand);
+ if (leadingZeros > 0)
+ {
+ significand <<= leadingZeros;
+ binaryExponent -= leadingZeros;
+ }
+
+ // Adjust exponent for the hidden bit in IEEE 754
+ int actualExponent = binaryExponent + exponentBias;
+
+ // Handle overflow to infinity
+ if (actualExponent >= TFloat.InfinityExponent)
+ {
+ return isNegative ? TFloat.NegativeInfinity : TFloat.PositiveInfinity;
+ }
+
+ // Handle underflow to zero or denormal
+ if (actualExponent <= 0)
+ {
+ // Denormal number
+ int denormalShift = 1 - actualExponent;
+ if (denormalShift >= 64)
+ {
+ return isNegative ? -TFloat.Zero : TFloat.Zero;
+ }
+ significand >>= denormalShift;
+ actualExponent = 0;
+ }
+
+ // Round to nearest, ties to even
+ int roundBit = 64 - mantissaBits - 1;
+ ulong mask = (1UL << roundBit) - 1;
+ ulong roundingBits = significand & mask;
+ significand >>= roundBit;
+
+ // Round to nearest, ties to even
+ if (roundingBits > (1UL << (roundBit - 1)) ||
+ (roundingBits == (1UL << (roundBit - 1)) && (significand & 1) == 1))
+ {
+ significand++;
+ if ((significand >> (mantissaBits + 1)) != 0)
+ {
+ // Overflow in rounding
+ significand >>= 1;
+ actualExponent++;
+ if (actualExponent >= TFloat.InfinityExponent)
+ {
+ return isNegative ? TFloat.NegativeInfinity : TFloat.PositiveInfinity;
+ }
+ }
+ }
+
+ // Remove the implicit leading bit
+ ulong mantissa = significand & TFloat.NormalMantissaMask;
+
+ // Construct the final bit representation
+ ulong bits = ((ulong)actualExponent << mantissaBits) | mantissa;
+
+ TFloat result = TFloat.BitsToFloat(bits);
+ return isNegative ? -result : result;
+ }
+
internal static bool TryParseFloat(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out TFloat result)
where TChar : unmanaged, IUtfChar
where TFloat : unmanaged, IBinaryFloatParseAndFormatInfo
{
+ // Check if we should parse as hex float
+ if ((styles & NumberStyles.AllowHexSpecifier) != 0)
+ {
+ return TryParseHexFloat(value, styles, info, out result);
+ }
+
NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, stackalloc byte[TFloat.NumberBufferLength]);
if (!TryStringToNumber(value, styles, ref number, info))
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 6869aeb397ceb0..d42c7efc3186a3 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -9758,6 +9758,7 @@ public enum NumberStyles
Any = 511,
AllowHexSpecifier = 512,
HexNumber = 515,
+ HexFloat = 679,
AllowBinarySpecifier = 1024,
BinaryNumber = 1027,
}
From 30afcdc1ed2330799b3a30016d3005bb692d8441 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 11 Oct 2025 18:35:16 +0000
Subject: [PATCH 4/9] Add hex float formatting and basic tests
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../src/System/Number.Formatting.cs | 133 ++++++++++++++++++
.../System/DoubleTests.cs | 10 ++
2 files changed, 143 insertions(+)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
index 2062aa526cb386..07359a9e220ab6 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
@@ -526,6 +526,132 @@ private static int GetFloatingPointMaxDigitsAndPrecision(char fmt, ref int preci
return maxDigits;
}
+ private static void FormatFloatAsHex(ref ValueListBuilder vlb, TNumber value, char fmt, int precision, NumberFormatInfo info)
+ where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo
+ where TChar : unmanaged, IUtfChar
+ {
+ // Get the raw bits
+ ulong bits = TNumber.FloatToBits(value);
+ int mantissaBits = TNumber.NormalMantissaBits;
+ int exponentBias = TNumber.ExponentBias;
+
+ // Extract sign, exponent, and mantissa
+ bool isNegative = (bits >> (mantissaBits + TNumber.ExponentBits)) != 0;
+ int biasedExponent = (int)((bits >> mantissaBits) & ((1UL << TNumber.ExponentBits) - 1));
+ ulong mantissa = bits & TNumber.NormalMantissaMask;
+
+ // Add sign
+ if (isNegative)
+ {
+ vlb.Append(TChar.CastFrom('-'));
+ }
+
+ // Add "0x" prefix
+ vlb.Append(TChar.CastFrom('0'));
+ vlb.Append(TChar.CastFrom(fmt)); // 'x' or 'X'
+
+ // Handle special cases
+ if (biasedExponent == TNumber.InfinityExponent)
+ {
+ // Infinity or NaN - just output as 0
+ vlb.Append(TChar.CastFrom('0'));
+ vlb.Append(TChar.CastFrom('p'));
+ vlb.Append(TChar.CastFrom('+'));
+ vlb.Append(TChar.CastFrom('0'));
+ return;
+ }
+
+ if (biasedExponent == 0 && mantissa == 0)
+ {
+ // Zero
+ vlb.Append(TChar.CastFrom('0'));
+ if (precision > 0)
+ {
+ vlb.Append(TChar.CastFrom('.'));
+ for (int i = 0; i < precision; i++)
+ {
+ vlb.Append(TChar.CastFrom('0'));
+ }
+ }
+ vlb.Append(TChar.CastFrom('p'));
+ vlb.Append(TChar.CastFrom('+'));
+ vlb.Append(TChar.CastFrom('0'));
+ return;
+ }
+
+ // Normalize: add implicit leading 1 for normal numbers
+ int actualExponent;
+ if (biasedExponent == 0)
+ {
+ // Denormal number
+ actualExponent = 1 - exponentBias;
+ }
+ else
+ {
+ // Normal number - add implicit leading bit
+ mantissa |= (1UL << mantissaBits);
+ actualExponent = biasedExponent - exponentBias;
+ }
+
+ // Normalize mantissa so the leading bit is in the MSB position
+ int shift = 64 - mantissaBits - 1;
+ mantissa <<= shift;
+
+ // Output integer part (always "1" for normalized)
+ char hexBase = fmt == 'X' ? 'A' : 'a';
+ int firstNibble = (int)(mantissa >> 60);
+ vlb.Append(TChar.CastFrom((char)('0' + (firstNibble > 9 ? 0 : firstNibble))));
+ if (firstNibble > 9)
+ {
+ vlb.Append(TChar.CastFrom((char)(hexBase + firstNibble - 10)));
+ }
+
+ // Remove the first nibble
+ mantissa = (mantissa << 4) & 0xFFFFFFFFFFFFFFFF;
+
+ // Determine how many hex digits to output
+ int hexDigits = precision >= 0 ? precision : (mantissaBits + 3) / 4;
+
+ if (hexDigits > 0)
+ {
+ vlb.Append(TChar.CastFrom('.'));
+
+ for (int i = 0; i < hexDigits; i++)
+ {
+ int nibble = (int)(mantissa >> 60);
+ char hexChar = nibble < 10 ? (char)('0' + nibble) : (char)(hexBase + nibble - 10);
+ vlb.Append(TChar.CastFrom(hexChar));
+ mantissa = (mantissa << 4) & 0xFFFFFFFFFFFFFFFF;
+ }
+ }
+
+ // Output exponent
+ vlb.Append(TChar.CastFrom('p'));
+ if (actualExponent >= 0)
+ {
+ vlb.Append(TChar.CastFrom('+'));
+ }
+
+ // Format exponent as decimal
+ FormatInt32(ref vlb, actualExponent, 0, null, info);
+ }
+
+ private static void FormatInt32(ref ValueListBuilder vlb, int value, int precision, string? format, NumberFormatInfo info)
+ where TChar : unmanaged, IUtfChar
+ {
+ if (value < 0)
+ {
+ vlb.Append(TChar.CastFrom('-'));
+ value = -value;
+ }
+
+ string numStr = ((uint)value).ToString();
+ foreach (char c in numStr)
+ {
+ vlb.Append(TChar.CastFrom(c));
+ }
+ }
+
public static string FormatFloat(TNumber value, string? format, NumberFormatInfo info)
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo
{
@@ -598,6 +724,13 @@ public static bool TryFormatFloat(TNumber value, ReadOnlySpan Parse_Valid_TestData()
yield return new object[] { "NaN", NumberStyles.Any, invariantFormat, double.NaN };
yield return new object[] { "Infinity", NumberStyles.Any, invariantFormat, double.PositiveInfinity };
yield return new object[] { "-Infinity", NumberStyles.Any, invariantFormat, double.NegativeInfinity };
+
+ // Hex float tests
+ yield return new object[] { "0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0 };
+ yield return new object[] { "0x1.8p0", NumberStyles.HexFloat, invariantFormat, 1.5 };
+ yield return new object[] { "0x1.0p1", NumberStyles.HexFloat, invariantFormat, 2.0 };
+ yield return new object[] { "0x1.0p-1", NumberStyles.HexFloat, invariantFormat, 0.5 };
+ yield return new object[] { "0x0.8p0", NumberStyles.HexFloat, invariantFormat, 0.5 };
+ yield return new object[] { "-0x1.0p0", NumberStyles.HexFloat, invariantFormat, -1.0 };
+ yield return new object[] { "+0x1.0p0", NumberStyles.HexFloat, invariantFormat, 1.0 };
+ yield return new object[] { "0x1.921fb54442d18p+1", NumberStyles.HexFloat, invariantFormat, Math.PI };
}
[Theory]
From 6166f7ffe79e50197f3884b198cf56e77d646a99 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 12 Oct 2025 02:30:28 +0000
Subject: [PATCH 5/9] Address code review feedback: fix hot paths, formatting
bugs, and add comprehensive tests
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
---
.../System/Globalization/NumberFormatInfo.cs | 19 ++++-
.../src/System/Number.Formatting.cs | 82 +++++++++++++------
.../src/System/Number.Parsing.cs | 2 +-
.../System/DoubleTests.cs | 56 ++++++++++++-
.../System.Runtime.Tests/System/HalfTests.cs | 27 ++++++
.../System/SingleTests.cs | 28 +++++++
6 files changed, 182 insertions(+), 32 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs
index a2c9b04a74de75..c8663d14b6beda 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/NumberFormatInfo.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
@@ -830,13 +831,13 @@ internal static void ValidateParseStyleFloatingPoint(NumberStyles style)
// Check for undefined flags
if ((style & InvalidNumberStyles) != 0)
{
- throw new ArgumentException(SR.Argument_InvalidNumberStyles, nameof(style));
+ ThrowInvalidStyle();
}
// Binary specifier is not supported for floating point
if ((style & NumberStyles.AllowBinarySpecifier) != 0)
{
- throw new ArgumentException(SR.Arg_HexBinaryStylesNotSupported, nameof(style));
+ ThrowHexBinaryStylesNotSupported();
}
// When AllowHexSpecifier is used, only specific flags are allowed
@@ -846,9 +847,21 @@ internal static void ValidateParseStyleFloatingPoint(NumberStyles style)
NumberStyles invalidFlags = style & ~NumberStyles.HexFloat;
if (invalidFlags != 0)
{
- throw new ArgumentException(SR.Arg_InvalidHexBinaryStyle, nameof(style));
+ ThrowInvalidHexBinaryStyle();
}
}
+
+ [DoesNotReturn]
+ static void ThrowInvalidStyle() =>
+ throw new ArgumentException(SR.Argument_InvalidNumberStyles, nameof(style));
+
+ [DoesNotReturn]
+ static void ThrowHexBinaryStylesNotSupported() =>
+ throw new ArgumentException(SR.Arg_HexBinaryStylesNotSupported, nameof(style));
+
+ [DoesNotReturn]
+ static void ThrowInvalidHexBinaryStyle() =>
+ throw new ArgumentException(SR.Arg_InvalidHexBinaryStyle, nameof(style));
}
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
index 07359a9e220ab6..31eceaa98fa3cb 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs
@@ -526,7 +526,7 @@ private static int GetFloatingPointMaxDigitsAndPrecision(char fmt, ref int preci
return maxDigits;
}
- private static void FormatFloatAsHex(ref ValueListBuilder vlb, TNumber value, char fmt, int precision, NumberFormatInfo info)
+ private static void FormatFloatAsHex(ref ValueListBuilder vlb, TNumber value, char fmt, int precision)
where TNumber : unmanaged, IBinaryFloatParseAndFormatInfo
where TChar : unmanaged, IUtfChar
{
@@ -553,7 +553,7 @@ private static void FormatFloatAsHex(ref ValueListBuilder
// Handle special cases
if (biasedExponent == TNumber.InfinityExponent)
{
- // Infinity or NaN - just output as 0
+ // Infinity or NaN - just output as 0
vlb.Append(TChar.CastFrom('0'));
vlb.Append(TChar.CastFrom('p'));
vlb.Append(TChar.CastFrom('+'));
@@ -579,35 +579,43 @@ private static void FormatFloatAsHex(ref ValueListBuilder
return;
}
- // Normalize: add implicit leading 1 for normal numbers
+ // Normalize mantissa for hex output (leading digit should be 1.xxx in range [1, 2))
int actualExponent;
+ ulong significand;
+
if (biasedExponent == 0)
{
- // Denormal number
- actualExponent = 1 - exponentBias;
+ // Denormal number - normalize by shifting until we get leading 1
+ significand = mantissa;
+ int lz = BitOperations.LeadingZeroCount(significand) - (64 - mantissaBits);
+ significand <<= lz;
+ actualExponent = 1 - exponentBias - lz;
}
else
{
// Normal number - add implicit leading bit
- mantissa |= (1UL << mantissaBits);
+ significand = (1UL << mantissaBits) | mantissa;
actualExponent = biasedExponent - exponentBias;
}
- // Normalize mantissa so the leading bit is in the MSB position
- int shift = 64 - mantissaBits - 1;
- mantissa <<= shift;
+ // Shift significand so the leading 1 is at bit 60 (first nibble position)
+ // This ensures the integer part is 1.xxx
+ int shift = 63 - mantissaBits;
+ significand <<= shift;
+
+ // Adjust exponent for the shift (we divided by 2^shift, so add shift to exponent)
+ // But we also want the leading nibble at bit 60, so shift right by 3 more
+ significand >>= 3;
+ actualExponent += 3;
- // Output integer part (always "1" for normalized)
+ // Output integer part (should always be 1 for normalized form)
char hexBase = fmt == 'X' ? 'A' : 'a';
- int firstNibble = (int)(mantissa >> 60);
- vlb.Append(TChar.CastFrom((char)('0' + (firstNibble > 9 ? 0 : firstNibble))));
- if (firstNibble > 9)
- {
- vlb.Append(TChar.CastFrom((char)(hexBase + firstNibble - 10)));
- }
+ int firstNibble = (int)(significand >> 60);
+ char firstHexChar = firstNibble < 10 ? (char)('0' + firstNibble) : (char)(hexBase + firstNibble - 10);
+ vlb.Append(TChar.CastFrom(firstHexChar));
// Remove the first nibble
- mantissa = (mantissa << 4) & 0xFFFFFFFFFFFFFFFF;
+ significand <<= 4;
// Determine how many hex digits to output
int hexDigits = precision >= 0 ? precision : (mantissaBits + 3) / 4;
@@ -618,10 +626,10 @@ private static void FormatFloatAsHex(ref ValueListBuilder
for (int i = 0; i < hexDigits; i++)
{
- int nibble = (int)(mantissa >> 60);
+ int nibble = (int)(significand >> 60);
char hexChar = nibble < 10 ? (char)('0' + nibble) : (char)(hexBase + nibble - 10);
vlb.Append(TChar.CastFrom(hexChar));
- mantissa = (mantissa << 4) & 0xFFFFFFFFFFFFFFFF;
+ significand <<= 4;
}
}
@@ -632,11 +640,11 @@ private static void FormatFloatAsHex(ref ValueListBuilder
vlb.Append(TChar.CastFrom('+'));
}
- // Format exponent as decimal
- FormatInt32(ref vlb, actualExponent, 0, null, info);
+ // Format exponent as decimal without allocating string
+ FormatInt32ToValueListBuilder(ref vlb, actualExponent);
}
- private static void FormatInt32(ref ValueListBuilder vlb, int value, int precision, string? format, NumberFormatInfo info)
+ private static void FormatInt32ToValueListBuilder(ref ValueListBuilder vlb, int value)
where TChar : unmanaged, IUtfChar
{
if (value < 0)
@@ -645,10 +653,32 @@ private static void FormatInt32(ref ValueListBuilder vlb, int valu
value = -value;
}
- string numStr = ((uint)value).ToString();
- foreach (char c in numStr)
+ // Handle zero specially
+ if (value == 0)
+ {
+ vlb.Append(TChar.CastFrom('0'));
+ return;
+ }
+
+ // Format digits in reverse, then reverse the span
+ int startIndex = vlb.Length;
+ uint uvalue = (uint)value;
+
+ while (uvalue > 0)
+ {
+ vlb.Append(TChar.CastFrom((char)('0' + (uvalue % 10))));
+ uvalue /= 10;
+ }
+
+ // Reverse the digits
+ int endIndex = vlb.Length - 1;
+ while (startIndex < endIndex)
{
- vlb.Append(TChar.CastFrom(c));
+ TChar temp = vlb[startIndex];
+ vlb[startIndex] = vlb[endIndex];
+ vlb[endIndex] = temp;
+ startIndex++;
+ endIndex--;
}
}
@@ -727,7 +757,7 @@ public static bool TryFormatFloat(TNumber value, ReadOnlySpan(ReadOnlySpan value, N
// Parse "0x" or "0X" prefix
if (index + 1 >= value.Length ||
TChar.CastToUInt32(value[index]) != '0' ||
- (TChar.CastToUInt32(value[index + 1]) != 'x' && TChar.CastToUInt32(value[index + 1]) != 'X'))
+ (TChar.CastToUInt32(value[index + 1]) | 0x20) != 'x')
{
return false;
}
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs
index dbf13afe513c6c..7861e2e3ad1958 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs
@@ -332,7 +332,7 @@ public static IEnumerable