From 4ff1b9908454ec80efaa819669439e5ca0ca70b7 Mon Sep 17 00:00:00 2001 From: Prashanth Govindarajan Date: Thu, 25 Jun 2020 10:38:00 -0700 Subject: [PATCH] Half: An IEEE 754 compliant float16 type (#37630) Fixes https://github.com/dotnet/runtime/issues/936. A lot of this code is a port of what have in [corefxlab](https://github.com/dotnet/corefxlab/tree/master/src/System.Numerics.Experimental). --- .../src/Resources/Strings.resx | 3 + .../System.Private.CoreLib.Shared.projitems | 1 + .../src/System/BitConverter.cs | 12 + .../System.Private.CoreLib/src/System/Half.cs | 692 +++++++++++++ .../src/System/Number.DiyFp.cs | 22 + .../src/System/Number.Dragon4.cs | 30 + .../src/System/Number.Formatting.cs | 121 ++- .../src/System/Number.Grisu3.cs | 34 + .../src/System/Number.NumberBuffer.cs | 1 + .../Number.NumberToFloatingPointBits.cs | 22 +- .../src/System/Number.Parsing.cs | 102 ++ .../System.Runtime/ref/System.Runtime.cs | 47 + .../tests/System.Runtime.Tests.csproj | 1 + .../System.Runtime/tests/System/HalfTests.cs | 933 ++++++++++++++++++ 14 files changed, 2017 insertions(+), 4 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/Half.cs create mode 100644 src/libraries/System.Runtime/tests/System/HalfTests.cs diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index 671183f4096f24..52cce0d6db9f02 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -3751,4 +3751,7 @@ Type '{0}' returned by IDynamicInterfaceCastable is not an interface. + + Object must be of type Half. + diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 73eeb0b884c3d9..8de305221ba51a 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -338,6 +338,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/BitConverter.cs b/src/libraries/System.Private.CoreLib/src/System/BitConverter.cs index bf604f41a97d60..50f1e0e8fec10c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/BitConverter.cs +++ b/src/libraries/System.Private.CoreLib/src/System/BitConverter.cs @@ -499,5 +499,17 @@ public static unsafe float Int32BitsToSingle(int value) return *((float*)&value); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe short HalfToInt16Bits(Half value) + { + return *((short*)&value); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static unsafe Half Int16BitsToHalf(short value) + { + return *(Half*)&value; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs new file mode 100644 index 00000000000000..1fce4d1b6870ff --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -0,0 +1,692 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace System +{ + // Portions of the code implemented below are based on the 'Berkeley SoftFloat Release 3e' algorithms. + + /// + /// An IEEE 754 compliant float16 type. + /// + [StructLayout(LayoutKind.Sequential)] + public readonly struct Half : IComparable, IFormattable, IComparable, IEquatable, ISpanFormattable + { + private const NumberStyles DefaultParseStyle = NumberStyles.Float | NumberStyles.AllowThousands; + + // Constants for manipulating the private bit-representation + + private const ushort SignMask = 0x8000; + private const ushort SignShift = 15; + + private const ushort ExponentMask = 0x7C00; + private const ushort ExponentShift = 10; + + private const ushort SignificandMask = 0x03FF; + private const ushort SignificandShift = 0; + + private const ushort MinSign = 0; + private const ushort MaxSign = 1; + + private const ushort MinExponent = 0x00; + private const ushort MaxExponent = 0x1F; + + private const ushort MinSignificand = 0x0000; + private const ushort MaxSignificand = 0x03FF; + + // Constants representing the private bit-representation for various default values + + private const ushort PositiveZeroBits = 0x0000; + private const ushort NegativeZeroBits = 0x8000; + + private const ushort EpsilonBits = 0x0001; + + private const ushort PositiveInfinityBits = 0x7C00; + private const ushort NegativeInfinityBits = 0xFC00; + + private const ushort PositiveQNaNBits = 0x7E00; + private const ushort NegativeQNaNBits = 0xFE00; + + private const ushort MinValueBits = 0xFBFF; + private const ushort MaxValueBits = 0x7BFF; + + // Well-defined and commonly used values + + public static Half Epsilon => new Half(EpsilonBits); // 5.9604645E-08 + + public static Half PositiveInfinity => new Half(PositiveInfinityBits); // 1.0 / 0.0; + + public static Half NegativeInfinity => new Half(NegativeInfinityBits); // -1.0 / 0.0 + + public static Half NaN => new Half(NegativeQNaNBits); // 0.0 / 0.0 + + public static Half MinValue => new Half(MinValueBits); // -65504 + + public static Half MaxValue => new Half(MaxValueBits); // 65504 + + // We use these explicit definitions to avoid the confusion between 0.0 and -0.0. + private static readonly Half PositiveZero = new Half(PositiveZeroBits); // 0.0 + private static readonly Half NegativeZero = new Half(NegativeZeroBits); // -0.0 + + private readonly ushort _value; + + internal Half(ushort value) + { + _value = value; + } + + private Half(bool sign, ushort exp, ushort sig) + => _value = (ushort)(((sign ? 1 : 0) << SignShift) + (exp << ExponentShift) + sig); + + private sbyte Exponent + { + get + { + return (sbyte)((_value & ExponentMask) >> ExponentShift); + } + } + + private ushort Significand + { + get + { + return (ushort)((_value & SignificandMask) >> SignificandShift); + } + } + + public static bool operator <(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative && !AreZero(left, right); + } + return (short)(left._value) < (short)(right._value); + } + + public static bool operator >(Half left, Half right) + { + return right < left; + } + + public static bool operator <=(Half left, Half right) + { + if (IsNaN(left) || IsNaN(right)) + { + // IEEE defines that NaN is unordered with respect to everything, including itself. + return false; + } + + bool leftIsNegative = IsNegative(left); + + if (leftIsNegative != IsNegative(right)) + { + // When the signs of left and right differ, we know that left is less than right if it is + // the negative value. The exception to this is if both values are zero, in which case IEEE + // says they should be equal, even if the signs differ. + return leftIsNegative || AreZero(left, right); + } + return (short)(left._value) <= (short)(right._value); + } + + public static bool operator >=(Half left, Half right) + { + return right <= left; + } + + public static bool operator ==(Half left, Half right) + { + return left.Equals(right); + } + + public static bool operator !=(Half left, Half right) + { + return !(left.Equals(right)); + } + + /// Determines whether the specified value is finite (zero, subnormal, or normal). + public static bool IsFinite(Half value) + { + return StripSign(value) < PositiveInfinityBits; + } + + /// Determines whether the specified value is infinite. + public static bool IsInfinity(Half value) + { + return StripSign(value) == PositiveInfinityBits; + } + + /// Determines whether the specified value is NaN. + public static bool IsNaN(Half value) + { + return StripSign(value) > PositiveInfinityBits; + } + + /// Determines whether the specified value is negative. + public static bool IsNegative(Half value) + { + return (short)(value._value) < 0; + } + + /// Determines whether the specified value is negative infinity. + public static bool IsNegativeInfinity(Half value) + { + return value._value == NegativeInfinityBits; + } + + /// Determines whether the specified value is normal. + // This is probably not worth inlining, it has branches and should be rarely called + public static bool IsNormal(Half value) + { + uint absValue = StripSign(value); + return (absValue < PositiveInfinityBits) // is finite + && (absValue != 0) // is not zero + && ((absValue & ExponentMask) != 0); // is not subnormal (has a non-zero exponent) + } + + /// Determines whether the specified value is positive infinity. + public static bool IsPositiveInfinity(Half value) + { + return value._value == PositiveInfinityBits; + } + + /// Determines whether the specified value is subnormal. + // This is probably not worth inlining, it has branches and should be rarely called + public static bool IsSubnormal(Half value) + { + uint absValue = StripSign(value); + return (absValue < PositiveInfinityBits) // is finite + && (absValue != 0) // is not zero + && ((absValue & ExponentMask) == 0); // is subnormal (has a zero exponent) + } + + /// + /// Parses a from a in the default parse style. + /// + /// The input to be parsed. + /// The equivalent value representing the input string. If the input exceeds Half's range, a or is returned. + public static Half Parse(string s) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return Number.ParseHalf(s, NumberStyles.Float | NumberStyles.AllowThousands, NumberFormatInfo.CurrentInfo); + } + + /// + /// Parses a from a in the given . + /// + /// The input to be parsed. + /// The used to parse the input. + /// The equivalent value representing the input string. If the input exceeds Half's range, a or is returned. + public static Half Parse(string s, NumberStyles style) + { + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return Number.ParseHalf(s, style, NumberFormatInfo.CurrentInfo); + } + + /// + /// Parses a from a and . + /// + /// The input to be parsed. + /// A format provider. + /// The equivalent value representing the input string. If the input exceeds Half's range, a or is returned. + public static Half Parse(string s, IFormatProvider? provider) + { + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return Number.ParseHalf(s, NumberStyles.Float | NumberStyles.AllowThousands, NumberFormatInfo.GetInstance(provider)); + } + + /// + /// Parses a from a with the given and . + /// + /// The input to be parsed. + /// The used to parse the input. + /// A format provider. + /// The equivalent value representing the input string. If the input exceeds Half's range, a or is returned. + public static Half Parse(string s, NumberStyles style = DefaultParseStyle, IFormatProvider? provider = null) + { + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); + if (s == null) ThrowHelper.ThrowArgumentNullException(ExceptionArgument.s); + return Number.ParseHalf(s, style, NumberFormatInfo.GetInstance(provider)); + } + + /// + /// Parses a from a and . + /// + /// The input to be parsed. + /// The used to parse the input. + /// A format provider. + /// The equivalent value representing the input string. If the input exceeds Half's range, a or is returned. + public static Half Parse(ReadOnlySpan s, NumberStyles style = DefaultParseStyle, IFormatProvider? provider = null) + { + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); + return Number.ParseHalf(s, style, NumberFormatInfo.GetInstance(provider)); + } + + /// + /// Tries to parses a from a in the default parse style. + /// + /// The input to be parsed. + /// The equivalent value representing the input string if the parse was successful. If the input exceeds Half's range, a or is returned. If the parse was unsuccessful, a default value is returned. + /// if the parse was successful, otherwise. + public static bool TryParse([NotNullWhen(true)] string? s, out Half result) + { + if (s == null) + { + result = default; + return false; + } + return TryParse(s, DefaultParseStyle, provider: null, out result); + } + + /// + /// Tries to parses a from a in the default parse style. + /// + /// The input to be parsed. + /// The equivalent value representing the input string if the parse was successful. If the input exceeds Half's range, a or is returned. If the parse was unsuccessful, a default value is returned. + /// if the parse was successful, otherwise. + public static bool TryParse(ReadOnlySpan s, out Half result) + { + return TryParse(s, DefaultParseStyle, provider: null, out result); + } + + /// + /// Tries to parse a from a with the given and . + /// + /// The input to be parsed. + /// The used to parse the input. + /// A format provider. + /// The equivalent value representing the input string if the parse was successful. If the input exceeds Half's range, a or is returned. If the parse was unsuccessful, a default value is returned. + /// if the parse was successful, otherwise. + public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out Half result) + { + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); + + if (s == null) + { + result = default; + return false; + } + + return TryParse(s.AsSpan(), style, provider, out result); + } + + /// + /// Tries to parse a from a with the given and . + /// + /// The input to be parsed. + /// The used to parse the input. + /// A format provider. + /// The equivalent value representing the input string if the parse was successful. If the input exceeds Half's range, a or is returned. If the parse was unsuccessful, a default value is returned. + /// if the parse was successful, otherwise. + public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out Half result) + { + NumberFormatInfo.ValidateParseStyleFloatingPoint(style); + return Number.TryParseHalf(s, style, NumberFormatInfo.GetInstance(provider), out result); + } + + private static bool AreZero(Half left, Half right) + { + // IEEE defines that positive and negative zero are equal, this gives us a quick equality check + // for two values by or'ing the private bits together and stripping the sign. They are both zero, + // and therefore equivalent, if the resulting value is still zero. + return (ushort)((left._value | right._value) & ~SignMask) == 0; + } + + private static bool IsNaNOrZero(Half value) + { + return ((value._value - 1) & ~SignMask) >= PositiveInfinityBits; + } + + private static uint StripSign(Half value) + { + return (ushort)(value._value & ~SignMask); + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + /// Thrown when is not of type . + public int CompareTo(object? obj) + { + if (!(obj is Half)) + { + return (obj is null) ? 1 : throw new ArgumentException(SR.Arg_MustBeHalf); + } + return CompareTo((Half)(obj)); + } + + /// + /// Compares this object to another object, returning an integer that indicates the relationship. + /// + /// A value less than zero if this is less than , zero if this is equal to , or a value greater than zero if this is greater than . + public int CompareTo(Half other) + { + if (this < other) + { + return -1; + } + + if (this > other) + { + return 1; + } + + if (this == other) + { + return 0; + } + + if (IsNaN(this)) + { + return IsNaN(other) ? 0 : -1; + } + + Debug.Assert(IsNaN(other)); + return 1; + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified . + /// + public override bool Equals(object? obj) + { + return (obj is Half other) && Equals(other); + } + + /// + /// Returns a value that indicates whether this instance is equal to a specified value. + /// + public bool Equals(Half other) + { + if (IsNaN(this) || IsNaN(other)) + { + // IEEE defines that NaN is not equal to anything, including itself. + return false; + } + + // IEEE defines that positive and negative zero are equivalent. + return (_value == other._value) || AreZero(this, other); + } + + /// + /// Serves as the default hash function. + /// + public override int GetHashCode() + { + if (IsNaNOrZero(this)) + { + // All NaNs should have the same hash code, as should both Zeros. + return _value & PositiveInfinityBits; + } + return _value; + } + + /// + /// Returns a string representation of the current value. + /// + public override string ToString() + { + return Number.FormatHalf(this, null, NumberFormatInfo.CurrentInfo); + } + + /// + /// Returns a string representation of the current value using the specified . + /// + public string ToString(string? format) + { + return Number.FormatHalf(this, format, NumberFormatInfo.CurrentInfo); + } + + /// + /// Returns a string representation of the current value with the specified . + /// + public string ToString(IFormatProvider? provider) + { + return Number.FormatHalf(this, null, NumberFormatInfo.GetInstance(provider)); + } + + /// + /// Returns a string representation of the current value using the specified and . + /// + public string ToString(string? format, IFormatProvider? provider) + { + return Number.FormatHalf(this, format, NumberFormatInfo.GetInstance(provider)); + } + + /// + /// Tries to format the value of the current Half instance into the provided span of characters. + /// + /// When this method returns, this instance's value formatted as a span of characters. + /// When this method returns, the number of characters that were written in . + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for . + /// An optional object that supplies culture-specific formatting information for . + /// + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format = default, IFormatProvider? provider = null) + { + return Number.TryFormatHalf(this, format, NumberFormatInfo.GetInstance(provider), destination, out charsWritten); + } + + // -----------------------Start of to-half conversions------------------------- + + public static explicit operator Half(float value) + { + const int SingleMaxExponent = 0xFF; + + uint floatInt = (uint)BitConverter.SingleToInt32Bits(value); + bool sign = (floatInt & float.SignMask) >> float.SignShift != 0; + int exp = (int)(floatInt & float.ExponentMask) >> float.ExponentShift; + uint sig = floatInt & float.SignificandMask; + + if (exp == SingleMaxExponent) + { + if (sig != 0) // NaN + { + return CreateHalfNaN(sign, (ulong)sig << 41); // Shift the significand bits to the left end + } + return sign ? NegativeInfinity : PositiveInfinity; + } + + uint sigHalf = sig >> 9 | ((sig & 0x1FFU) != 0 ? 1U : 0U); // RightShiftJam + + if ((exp | (int)sigHalf) == 0) + { + return new Half(sign, 0, 0); + } + + return new Half(RoundPackToHalf(sign, (short)(exp - 0x71), (ushort)(sigHalf | 0x4000))); + } + + public static explicit operator Half(double value) + { + const int DoubleMaxExponent = 0x7FF; + + ulong doubleInt = (ulong)BitConverter.DoubleToInt64Bits(value); + bool sign = (doubleInt & double.SignMask) >> double.SignShift != 0; + int exp = (int)((doubleInt & double.ExponentMask) >> double.ExponentShift); + ulong sig = doubleInt & double.SignificandMask; + + if (exp == DoubleMaxExponent) + { + if (sig != 0) // NaN + { + return CreateHalfNaN(sign, sig << 12); // Shift the significand bits to the left end + } + return sign ? NegativeInfinity : PositiveInfinity; + } + + uint sigHalf = (uint)ShiftRightJam(sig, 38); + if ((exp | (int)sigHalf) == 0) + { + return new Half(sign, 0, 0); + } + return new Half(RoundPackToHalf(sign, (short)(exp - 0x3F1), (ushort)(sigHalf | 0x4000))); + } + + // -----------------------Start of from-half conversions------------------------- + public static explicit operator float(Half value) + { + bool sign = IsNegative(value); + int exp = value.Exponent; + uint sig = value.Significand; + + if (exp == MaxExponent) + { + if (sig != 0) + { + return CreateSingleNaN(sign, (ulong)sig << 54); + } + return sign ? float.NegativeInfinity : float.PositiveInfinity; + } + + if (exp == 0) + { + if (sig == 0) + { + return BitConverter.Int32BitsToSingle((int)(sign ? float.SignMask : 0)); // Positive / Negative zero + } + (exp, sig) = NormSubnormalF16Sig(sig); + exp -= 1; + } + + return CreateSingle(sign, (byte)(exp + 0x70), sig << 13); + } + + public static explicit operator double(Half value) + { + bool sign = IsNegative(value); + int exp = value.Exponent; + uint sig = value.Significand; + + if (exp == MaxExponent) + { + if (sig != 0) + { + return CreateDoubleNaN(sign, (ulong)sig << 54); + } + return sign ? double.NegativeInfinity : double.PositiveInfinity; + } + + if (exp == 0) + { + if (sig == 0) + { + return BitConverter.Int64BitsToDouble((long)(sign ? double.SignMask : 0)); // Positive / Negative zero + } + (exp, sig) = NormSubnormalF16Sig(sig); + exp -= 1; + } + + return CreateDouble(sign, (ushort)(exp + 0x3F0), (ulong)sig << 42); + } + + // IEEE 754 specifies NaNs to be propagated + internal static Half Negate(Half value) + { + return IsNaN(value) ? value : new Half((ushort)(value._value ^ SignMask)); + } + + private static (int Exp, uint Sig) NormSubnormalF16Sig(uint sig) + { + int shiftDist = BitOperations.LeadingZeroCount(sig) - 16 - 5; + return (1 - shiftDist, sig << shiftDist); + } + + #region Utilities + + // Significand bits should be shifted towards to the left end before calling these methods + // Creates Quiet NaN if significand == 0 + private static Half CreateHalfNaN(bool sign, ulong significand) + { + const uint NaNBits = ExponentMask | 0x200; // Most significant significand bit + + uint signInt = (sign ? 1U : 0U) << SignShift; + uint sigInt = (uint)(significand >> 54); + + return BitConverter.Int16BitsToHalf((short)(signInt | NaNBits | sigInt)); + } + + private static ushort RoundPackToHalf(bool sign, short exp, ushort sig) + { + const int RoundIncrement = 0x8; // Depends on rounding mode but it's always towards closest / ties to even + int roundBits = sig & 0xF; + + if ((uint)exp >= 0x1D) + { + if (exp < 0) + { + sig = (ushort)ShiftRightJam(sig, -exp); + exp = 0; + } + else if (exp > 0x1D || sig + RoundIncrement >= 0x8000) // Overflow + { + return sign ? NegativeInfinityBits : PositiveInfinityBits; + } + } + + sig = (ushort)((sig + RoundIncrement) >> 4); + sig &= (ushort)~(((roundBits ^ 8) != 0 ? 0 : 1) & 1); + + if (sig == 0) + { + exp = 0; + } + + return new Half(sign, (ushort)exp, sig)._value; + } + + // If any bits are lost by shifting, "jam" them into the LSB. + // if dist > bit count, Will be 1 or 0 depending on i + // (unlike bitwise operators that masks the lower 5 bits) + private static uint ShiftRightJam(uint i, int dist) + => dist < 31 ? (i >> dist) | (i << (-dist & 31) != 0 ? 1U : 0U) : (i != 0 ? 1U : 0U); + + private static ulong ShiftRightJam(ulong l, int dist) + => dist < 63 ? (l >> dist) | (l << (-dist & 63) != 0 ? 1UL : 0UL) : (l != 0 ? 1UL : 0UL); + + private static float CreateSingleNaN(bool sign, ulong significand) + { + const uint NaNBits = float.ExponentMask | 0x400000; // Most significant significand bit + + uint signInt = (sign ? 1U : 0U) << float.SignShift; + uint sigInt = (uint)(significand >> 41); + + return BitConverter.Int32BitsToSingle((int)(signInt | NaNBits | sigInt)); + } + + private static double CreateDoubleNaN(bool sign, ulong significand) + { + const ulong NaNBits = double.ExponentMask | 0x80000_00000000; // Most significant significand bit + + ulong signInt = (sign ? 1UL : 0UL) << double.SignShift; + ulong sigInt = significand >> 12; + + return BitConverter.Int64BitsToDouble((long)(signInt | NaNBits | sigInt)); + } + + private static float CreateSingle(bool sign, byte exp, uint sig) + => BitConverter.Int32BitsToSingle((int)(((sign ? 1U : 0U) << float.SignShift) | ((uint)exp << float.ExponentShift) | sig)); + + private static double CreateDouble(bool sign, ushort exp, ulong sig) + => BitConverter.Int64BitsToDouble((long)(((sign ? 1UL : 0UL) << double.SignShift) | ((ulong)exp << double.ExponentShift) | sig)); + + #endregion + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs b/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs index 141690dd6325bf..2b2e34ec9065aa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.DiyFp.cs @@ -20,6 +20,7 @@ internal readonly ref struct DiyFp { public const int DoubleImplicitBitIndex = 52; public const int SingleImplicitBitIndex = 23; + public const int HalfImplicitBitIndex = 10; public const int SignificandSize = 64; @@ -54,6 +55,20 @@ public static DiyFp CreateAndGetBoundaries(float value, out DiyFp mMinus, out Di return result; } + // Computes the two boundaries of value. + // + // The bigger boundary (mPlus) is normalized. + // The lower boundary has the same exponent as mPlus. + // + // Precondition: + // The value encoded by value must be greater than 0. + public static DiyFp CreateAndGetBoundaries(Half value, out DiyFp mMinus, out DiyFp mPlus) + { + var result = new DiyFp(value); + result.GetBoundaries(HalfImplicitBitIndex, out mMinus, out mPlus); + return result; + } + public DiyFp(double value) { Debug.Assert(double.IsFinite(value)); @@ -68,6 +83,13 @@ public DiyFp(float value) f = ExtractFractionAndBiasedExponent(value, out e); } + public DiyFp(Half value) + { + Debug.Assert(Half.IsFinite(value)); + Debug.Assert((float)value > 0.0f); + f = ExtractFractionAndBiasedExponent(value, out e); + } + public DiyFp(ulong f, int e) { this.f = f; diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs index 3e03b8b4972cd7..d753124b3de0ad 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Dragon4.cs @@ -41,6 +41,36 @@ public static void Dragon4Double(double value, int cutoffNumber, bool isSignific number.DigitsCount = length; } + public static unsafe void Dragon4Half(Half value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number) + { + Half v = Half.IsNegative(value) ? Half.Negate(value) : value; + + Debug.Assert((double)v > 0.0); + Debug.Assert(Half.IsFinite(v)); + + ushort mantissa = ExtractFractionAndBiasedExponent(value, out int exponent); + + uint mantissaHighBitIdx; + bool hasUnequalMargins = false; + + if ((mantissa >> DiyFp.HalfImplicitBitIndex) != 0) + { + mantissaHighBitIdx = DiyFp.HalfImplicitBitIndex; + hasUnequalMargins = (mantissa == (1U << DiyFp.HalfImplicitBitIndex)); + } + else + { + Debug.Assert(mantissa != 0); + mantissaHighBitIdx = (uint)BitOperations.Log2(mantissa); + } + + int length = (int)(Dragon4(mantissa, exponent, mantissaHighBitIdx, hasUnequalMargins, cutoffNumber, isSignificantDigits, number.Digits, out int decimalExponent)); + + number.Scale = decimalExponent + 1; + number.Digits[length] = (byte)('\0'); + number.DigitsCount = length; + } + public static unsafe void Dragon4Single(float value, int cutoffNumber, bool isSignificantDigits, ref NumberBuffer number) { float v = float.IsNegative(value) ? -value : value; 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 6f2f7b15d7c2d7..4b61b427d64b6b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Formatting.cs @@ -247,6 +247,7 @@ internal static partial class Number // SinglePrecision and DoublePrecision represent the maximum number of digits required // to guarantee that any given Single or Double can roundtrip. Some numbers may require // less, but none will require more. + private const int HalfPrecision = 5; private const int SinglePrecision = 9; private const int DoublePrecision = 17; @@ -256,6 +257,7 @@ internal static partial class Number // In order to support more digits, we would need to update ParseFormatSpecifier to pre-parse // the format and determine exactly how many digits are being requested and whether they // represent "significant digits" or "digits after the decimal point". + private const int HalfPrecisionCustomFormat = 5; private const int SinglePrecisionCustomFormat = 7; private const int DoublePrecisionCustomFormat = 15; @@ -631,7 +633,7 @@ public static bool TryFormatSingle(float value, ReadOnlySpan format, Numbe // accept values like 0 and others may require additional fixups. int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits); - if ((value != 0.0f) && (!isSignificantDigits || !Grisu3.TryRunSingle(value, precision, ref number))) + if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunSingle(value, precision, ref number))) { Dragon4Single(value, precision, isSignificantDigits, ref number); } @@ -668,6 +670,91 @@ public static bool TryFormatSingle(float value, ReadOnlySpan format, Numbe return null; } + public static string FormatHalf(Half value, string? format, NumberFormatInfo info) + { + var sb = new ValueStringBuilder(stackalloc char[CharStackBufferSize]); + return FormatHalf(ref sb, value, format, info) ?? sb.ToString(); + } + + /// Formats the specified value according to the specified format and info. + /// + /// Non-null if an existing string can be returned, in which case the builder will be unmodified. + /// Null if no existing string was returned, in which case the formatted output is in the builder. + /// + private static unsafe string? FormatHalf(ref ValueStringBuilder sb, Half value, ReadOnlySpan format, NumberFormatInfo info) + { + if (!Half.IsFinite(value)) + { + if (Half.IsNaN(value)) + { + return info.NaNSymbol; + } + + return Half.IsNegative(value) ? info.NegativeInfinitySymbol : info.PositiveInfinitySymbol; + } + + char fmt = ParseFormatSpecifier(format, out int precision); + byte* pDigits = stackalloc byte[HalfNumberBufferLength]; + + if (fmt == '\0') + { + precision = HalfPrecisionCustomFormat; + } + + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, HalfNumberBufferLength); + number.IsNegative = Half.IsNegative(value); + + // We need to track the original precision requested since some formats + // accept values like 0 and others may require additional fixups. + int nMaxDigits = GetFloatingPointMaxDigitsAndPrecision(fmt, ref precision, info, out bool isSignificantDigits); + + if ((value != default) && (!isSignificantDigits || !Grisu3.TryRunHalf(value, precision, ref number))) + { + Dragon4Half(value, precision, isSignificantDigits, ref number); + } + + number.CheckConsistency(); + + // When the number is known to be roundtrippable (either because we requested it be, or + // because we know we have enough digits to satisfy roundtrippability), we should validate + // that the number actually roundtrips back to the original result. + + Debug.Assert(((precision != -1) && (precision < HalfPrecision)) || (BitConverter.HalfToInt16Bits(value) == BitConverter.HalfToInt16Bits(NumberToHalf(ref number)))); + + if (fmt != 0) + { + if (precision == -1) + { + Debug.Assert((fmt == 'G') || (fmt == 'g') || (fmt == 'R') || (fmt == 'r')); + + // For the roundtrip and general format specifiers, when returning the shortest roundtrippable + // string, we need to update the maximum number of digits to be the greater of number.DigitsCount + // or SinglePrecision. This ensures that we continue returning "pretty" strings for values with + // less digits. One example this fixes is "-60", which would otherwise be formatted as "-6E+01" + // since DigitsCount would be 1 and the formatter would almost immediately switch to scientific notation. + + nMaxDigits = Math.Max(number.DigitsCount, HalfPrecision); + } + NumberToString(ref sb, ref number, fmt, nMaxDigits, info); + } + else + { + Debug.Assert(precision == HalfPrecisionCustomFormat); + NumberToStringFormat(ref sb, ref number, format, info); + } + return null; + } + + public static bool TryFormatHalf(Half value, ReadOnlySpan format, NumberFormatInfo info, Span destination, out int charsWritten) + { + var sb = new ValueStringBuilder(stackalloc char[CharStackBufferSize]); + string? s = FormatHalf(ref sb, value, format, info); + return s != null ? + TryCopyTo(s, destination, out charsWritten) : + sb.TryCopyTo(destination, out charsWritten); + } + + private static bool TryCopyTo(string source, Span destination, out int charsWritten) { Debug.Assert(source != null); @@ -2563,6 +2650,38 @@ private static ulong ExtractFractionAndBiasedExponent(double value, out int expo return fraction; } + private static ushort ExtractFractionAndBiasedExponent(Half value, out int exponent) + { + ushort bits = (ushort)BitConverter.HalfToInt16Bits(value); + ushort fraction = (ushort)(bits & 0x3FF); + exponent = ((int)(bits >> 10) & 0x1F); + + if (exponent != 0) + { + // For normalized value, according to https://en.wikipedia.org/wiki/Half-precision_floating-point_format + // value = 1.fraction * 2^(exp - 15) + // = (1 + mantissa / 2^10) * 2^(exp - 15) + // = (2^10 + mantissa) * 2^(exp - 15 - 10) + // + // So f = (2^10 + mantissa), e = exp - 25; + + fraction |= (ushort)(1U << 10); + exponent -= 25; + } + else + { + // For denormalized value, according to https://en.wikipedia.org/wiki/Half-precision_floating-point_format + // value = 0.fraction * 2^(1 - 15) + // = (mantissa / 2^10) * 2^(-14) + // = mantissa * 2^(-14 - 10) + // = mantissa * 2^(-24) + // So f = mantissa, e = -24 + exponent = -24; + } + + return fraction; + } + private static uint ExtractFractionAndBiasedExponent(float value, out int exponent) { uint bits = (uint)(BitConverter.SingleToInt32Bits(value)); diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs b/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs index 528e64e0aa7387..064d9fec187bfa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Grisu3.cs @@ -356,6 +356,40 @@ public static bool TryRunDouble(double value, int requestedDigits, ref NumberBuf return result; } + public static bool TryRunHalf(Half value, int requestedDigits, ref NumberBuffer number) + { + Half v = Half.IsNegative(value) ? Half.Negate(value) : value; + + Debug.Assert((double)v > 0); + Debug.Assert(Half.IsFinite(v)); + + int length; + int decimalExponent; + bool result; + + if (requestedDigits == -1) + { + DiyFp w = DiyFp.CreateAndGetBoundaries(v, out DiyFp boundaryMinus, out DiyFp boundaryPlus).Normalize(); + result = TryRunShortest(in boundaryMinus, in w, in boundaryPlus, number.Digits, out length, out decimalExponent); + } + else + { + DiyFp w = new DiyFp(v).Normalize(); + result = TryRunCounted(in w, requestedDigits, number.Digits, out length, out decimalExponent); + } + + if (result) + { + Debug.Assert((requestedDigits == -1) || (length == requestedDigits)); + + number.Scale = length + decimalExponent; + number.Digits[length] = (byte)('\0'); + number.DigitsCount = length; + } + + return result; + } + public static bool TryRunSingle(float value, int requestedDigits, ref NumberBuffer number) { float v = float.IsNegative(value) ? -value : value; diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.NumberBuffer.cs b/src/libraries/System.Private.CoreLib/src/System/Number.NumberBuffer.cs index 7218af24076fff..b258efc74aa3cf 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.NumberBuffer.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.NumberBuffer.cs @@ -16,6 +16,7 @@ internal static partial class Number internal const int Int32NumberBufferLength = 10 + 1; // 10 for the longest input: 2,147,483,647 internal const int Int64NumberBufferLength = 19 + 1; // 19 for the longest input: 9,223,372,036,854,775,807 internal const int SingleNumberBufferLength = 112 + 1 + 1; // 112 for the longest input + 1 for rounding: 1.40129846E-45 + internal const int HalfNumberBufferLength = 21; // 19 for the longest input + 1 for rounding (+1 for the null terminator) internal const int UInt32NumberBufferLength = 10 + 1; // 10 for the longest input: 4,294,967,295 internal const int UInt64NumberBufferLength = 20 + 1; // 20 for the longest input: 18,446,744,073,709,551,615 diff --git a/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs b/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs index 59a47c67455e6a..5563441a0a5249 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.NumberToFloatingPointBits.cs @@ -26,6 +26,14 @@ public readonly struct FloatingPointInfo infinityBits: 0x7F800000 ); + public static readonly FloatingPointInfo Half = new FloatingPointInfo( + denormalMantissaBits: 10, + exponentBits: 5, + maxBinaryExponent: 15, + exponentBias: 15, + infinityBits: 0x7C00 + ); + public ulong ZeroBits { get; } public ulong InfinityBits { get; } @@ -365,7 +373,7 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number, in Float byte* src = number.GetDigitsPointer(); - if ((info.DenormalMantissaBits == 23) && (totalDigits <= 7) && (fastExponent <= 10)) + if ((info.DenormalMantissaBits <= 23) && (totalDigits <= 7) && (fastExponent <= 10)) { // It is only valid to do this optimization for single-precision floating-point // values since we can lose some of the mantissa bits and would return the @@ -383,6 +391,10 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number, in Float result *= scale; } + if (info.DenormalMantissaBits == 10) + { + return (ushort)(BitConverter.HalfToInt16Bits((Half)result)); + } return (uint)(BitConverter.SingleToInt32Bits(result)); } @@ -404,11 +416,15 @@ private static ulong NumberToFloatingPointBits(ref NumberBuffer number, in Float { return (ulong)(BitConverter.DoubleToInt64Bits(result)); } - else + else if (info.DenormalMantissaBits == 23) { - Debug.Assert(info.DenormalMantissaBits == 23); return (uint)(BitConverter.SingleToInt32Bits((float)(result))); } + else + { + Debug.Assert(info.DenormalMantissaBits == 10); + return (uint)(BitConverter.HalfToInt16Bits((Half)(result))); + } } return NumberToFloatingPointBitsSlow(ref number, in info, positiveExponent, integerDigitsPresent, fractionalDigitsPresent); 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 8c4cd1c9adff5e..fb2949089846bd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Number.Parsing.cs @@ -42,6 +42,9 @@ internal static partial class Number private const int SingleMaxExponent = 39; private const int SingleMinExponent = -45; + private const int HalfMaxExponent = 5; + private const int HalfMinExponent = -8; + /// Map from an ASCII char to its hex value, e.g. arr['b'] == 11. 0xFF means it's not a hex digit. internal static ReadOnlySpan CharToHexLookup => new byte[] { @@ -1707,6 +1710,16 @@ internal static float ParseSingle(ReadOnlySpan value, NumberStyles styles, return result; } + internal static Half ParseHalf(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info) + { + if (!TryParseHalf(value, styles, info, out Half result)) + { + ThrowOverflowOrFormatException(ParsingStatus.Failed); + } + + return result; + } + internal static unsafe ParsingStatus TryParseDecimal(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out decimal result) { byte* pDigits = stackalloc byte[DecimalNumberBufferLength]; @@ -1789,6 +1802,73 @@ internal static unsafe bool TryParseDouble(ReadOnlySpan value, NumberStyle return true; } + internal static unsafe bool TryParseHalf(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out Half result) + { + byte* pDigits = stackalloc byte[HalfNumberBufferLength]; + NumberBuffer number = new NumberBuffer(NumberBufferKind.FloatingPoint, pDigits, HalfNumberBufferLength); + + if (!TryStringToNumber(value, styles, ref number, info)) + { + ReadOnlySpan valueTrim = value.Trim(); + + // This code would be simpler if we only had the concept of `InfinitySymbol`, but + // we don't so we'll check the existing cases first and then handle `PositiveSign` + + // `PositiveInfinitySymbol` and `PositiveSign/NegativeSign` + `NaNSymbol` last. + // + // Additionally, since some cultures ("wo") actually define `PositiveInfinitySymbol` + // to include `PositiveSign`, we need to check whether `PositiveInfinitySymbol` fits + // that case so that we don't start parsing things like `++infini`. + + if (valueTrim.EqualsOrdinalIgnoreCase(info.PositiveInfinitySymbol)) + { + result = Half.PositiveInfinity; + } + else if (valueTrim.EqualsOrdinalIgnoreCase(info.NegativeInfinitySymbol)) + { + result = Half.NegativeInfinity; + } + else if (valueTrim.EqualsOrdinalIgnoreCase(info.NaNSymbol)) + { + result = Half.NaN; + } + else if (valueTrim.StartsWith(info.PositiveSign, StringComparison.OrdinalIgnoreCase)) + { + valueTrim = valueTrim.Slice(info.PositiveSign.Length); + + if (!info.PositiveInfinitySymbol.StartsWith(info.PositiveSign, StringComparison.OrdinalIgnoreCase) && valueTrim.EqualsOrdinalIgnoreCase(info.PositiveInfinitySymbol)) + { + result = Half.PositiveInfinity; + } + else if (!info.NaNSymbol.StartsWith(info.PositiveSign, StringComparison.OrdinalIgnoreCase) && valueTrim.EqualsOrdinalIgnoreCase(info.NaNSymbol)) + { + result = Half.NaN; + } + else + { + result = (Half)0; + return false; + } + } + else if (valueTrim.StartsWith(info.NegativeSign, StringComparison.OrdinalIgnoreCase) && + !info.NaNSymbol.StartsWith(info.NegativeSign, StringComparison.OrdinalIgnoreCase) && + valueTrim.Slice(info.NegativeSign.Length).EqualsOrdinalIgnoreCase(info.NaNSymbol)) + { + result = Half.NaN; + } + else + { + result = (Half)0; + return false; // We really failed + } + } + else + { + result = NumberToHalf(ref number); + } + + return true; + } + internal static unsafe bool TryParseSingle(ReadOnlySpan value, NumberStyles styles, NumberFormatInfo info, out float result) { byte* pDigits = stackalloc byte[SingleNumberBufferLength]; @@ -1999,6 +2079,28 @@ internal static double NumberToDouble(ref NumberBuffer number) return number.IsNegative ? -result : result; } + internal static Half NumberToHalf(ref NumberBuffer number) + { + number.CheckConsistency(); + Half result; + + if ((number.DigitsCount == 0) || (number.Scale < HalfMinExponent)) + { + result = default; + } + else if (number.Scale > HalfMaxExponent) + { + result = Half.PositiveInfinity; + } + else + { + ushort bits = (ushort)(NumberToFloatingPointBits(ref number, in FloatingPointInfo.Half)); + result = new Half(bits); + } + + return number.IsNegative ? Half.Negate(result) : result; + } + internal static float NumberToSingle(ref NumberBuffer number) { number.CheckConsistency(); diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 7fa3dac5f21062..b40ad7d9068e18 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -2194,6 +2194,53 @@ public partial struct Guid : System.IComparable, System.IComparable public static bool TryParseExact([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? format, out System.Guid result) { throw null; } public bool TryWriteBytes(System.Span destination) { throw null; } } +    public readonly partial struct Half : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable +    { +        private readonly int _dummyPrimitive; +        public static System.Half Epsilon { get { throw null; } } +        public static System.Half MaxValue { get { throw null; } } +        public static System.Half MinValue { get { throw null; } } +        public static System.Half NaN { get { throw null; } } +        public static System.Half NegativeInfinity { get { throw null; } } +        public static System.Half PositiveInfinity { get { throw null; } } +        public int CompareTo(System.Half other) { throw null; } +        public int CompareTo(object? obj) { throw null; } +        public bool Equals(System.Half other) { throw null; } +        public override bool Equals(object? obj) { throw null; } +        public override int GetHashCode() { throw null; } +        public static bool IsFinite(System.Half value) { throw null; } +        public static bool IsInfinity(System.Half value) { throw null; } +        public static bool IsNaN(System.Half value) { throw null; } +        public static bool IsNegative(System.Half value) { throw null; } +        public static bool IsNegativeInfinity(System.Half value) { throw null; } +        public static bool IsNormal(System.Half value) { throw null; } +        public static bool IsPositiveInfinity(System.Half value) { throw null; } +        public static bool IsSubnormal(System.Half value) { throw null; } +        public static bool operator ==(System.Half left, System.Half right) { throw null; } +        public static explicit operator System.Half (double value) { throw null; } +        public static explicit operator System.Half (float value) { throw null; } +        public static bool operator >(System.Half left, System.Half right) { throw null; } +        public static bool operator >=(System.Half left, System.Half right) { throw null; } +        public static explicit operator double (System.Half value) { throw null; } +        public static explicit operator float (System.Half value) { throw null; } +        public static bool operator !=(System.Half left, System.Half right) { throw null; } +        public static bool operator <(System.Half left, System.Half right) { throw null; } +        public static bool operator <=(System.Half left, System.Half right) { throw null; } +        public static System.Half Parse(System.ReadOnlySpan s, System.Globalization.NumberStyles style = System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowLeadingSign | System.Globalization.NumberStyles.AllowLeadingWhite | System.Globalization.NumberStyles.AllowThousands | System.Globalization.NumberStyles.AllowTrailingWhite, System.IFormatProvider? provider = null) { throw null; } +        public static System.Half Parse(string s) { throw null; } +        public static System.Half Parse(string s, System.Globalization.NumberStyles style) { throw null; } +        public static System.Half Parse(string s, System.Globalization.NumberStyles style = System.Globalization.NumberStyles.AllowDecimalPoint | System.Globalization.NumberStyles.AllowExponent | System.Globalization.NumberStyles.AllowLeadingSign | System.Globalization.NumberStyles.AllowLeadingWhite | System.Globalization.NumberStyles.AllowThousands | System.Globalization.NumberStyles.AllowTrailingWhite, System.IFormatProvider? provider = null) { throw null; } +        public static System.Half Parse(string s, System.IFormatProvider provider) { throw null; } +        public override string ToString() { throw null; } +        public string ToString(System.IFormatProvider? provider) { throw null; } +        public string ToString(string? format) { throw null; } +        public string ToString(string? format, System.IFormatProvider? provider) { throw null; } +        public bool TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? provider = null) { throw null; } +        public static bool TryParse(System.ReadOnlySpan s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Half result) { throw null; } +        public static bool TryParse(System.ReadOnlySpan s, out System.Half result) { throw null; } +        public static bool TryParse(string s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Half result) { throw null; } +        public static bool TryParse(string s, out System.Half result) { throw null; } +    } public partial struct HashCode { private int _dummyPrimitive; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index 69f4de93591a8a..8943d858f94dec 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -80,6 +80,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System/HalfTests.cs b/src/libraries/System.Runtime/tests/System/HalfTests.cs new file mode 100644 index 00000000000000..0a57cc2f1b49b8 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/HalfTests.cs @@ -0,0 +1,933 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Globalization; +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.Tests +{ + public partial class HalfTests + { + private static unsafe ushort HalfToUInt16Bits(Half value) + { + return *((ushort*)&value); + } + + private static unsafe Half UInt16BitsToHalf(ushort value) + { + return *((Half*)&value); + } + + [Fact] + public static void Epsilon() + { + Assert.Equal(0x0001u, HalfToUInt16Bits(Half.Epsilon)); + } + + [Fact] + public static void PositiveInfinity() + { + Assert.Equal(0x7C00u, HalfToUInt16Bits(Half.PositiveInfinity)); + } + + [Fact] + public static void NegativeInfinity() + { + Assert.Equal(0xFC00u, HalfToUInt16Bits(Half.NegativeInfinity)); + } + + [Fact] + public static void NaN() + { + Assert.Equal(0xFE00u, HalfToUInt16Bits(Half.NaN)); + } + + [Fact] + public static void MinValue() + { + Assert.Equal(0xFBFFu, HalfToUInt16Bits(Half.MinValue)); + } + + [Fact] + public static void MaxValue() + { + Assert.Equal(0x7BFFu, HalfToUInt16Bits(Half.MaxValue)); + } + + [Fact] + public static void Ctor_Empty() + { + var value = new Half(); + Assert.Equal(0x0000, HalfToUInt16Bits(value)); + } + + public static IEnumerable IsFinite_TestData() + { + yield return new object[] { Half.NegativeInfinity, false }; // Negative Infinity + yield return new object[] { Half.MinValue, true }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), true }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), true }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), true }; // Max Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8000), true }; // Negative Zero + yield return new object[] { Half.NaN, false }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), true }; // Positive Zero + yield return new object[] { Half.Epsilon, true }; // Min Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x03FF), true }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), true }; // Min Positive Normal + yield return new object[] { Half.MaxValue, true }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, false }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsFinite_TestData))] + public static void IsFinite(Half value, bool expected) + { + Assert.Equal(expected, Half.IsFinite(value)); + } + + public static IEnumerable IsInfinity_TestData() + { + yield return new object[] { Half.NegativeInfinity, true }; // Negative Infinity + yield return new object[] { Half.MinValue, false }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), false }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), false }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), false }; // Max Negative Subnormal (Negative Epsilon) + yield return new object[] { UInt16BitsToHalf(0x8000), false }; // Negative Zero + yield return new object[] { Half.NaN, false }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), false }; // Positive Zero + yield return new object[] { Half.Epsilon, false }; // Min Positive Subnormal (Positive Epsilon) + yield return new object[] { UInt16BitsToHalf(0x03FF), false }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), false }; // Min Positive Normal + yield return new object[] { Half.MaxValue, false }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, true }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsInfinity_TestData))] + public static void IsInfinity(Half value, bool expected) + { + Assert.Equal(expected, Half.IsInfinity(value)); + } + + public static IEnumerable IsNaN_TestData() + { + yield return new object[] { Half.NegativeInfinity, false }; // Negative Infinity + yield return new object[] { Half.MinValue, false }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), false }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), false }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), false }; // Max Negative Subnormal (Negative Epsilon) + yield return new object[] { UInt16BitsToHalf(0x8000), false }; // Negative Zero + yield return new object[] { Half.NaN, true }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), false }; // Positive Zero + yield return new object[] { Half.Epsilon, false }; // Min Positive Subnormal (Positive Epsilon) + yield return new object[] { UInt16BitsToHalf(0x03FF), false }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), false }; // Min Positive Normal + yield return new object[] { Half.MaxValue, false }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, false }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsNaN_TestData))] + public static void IsNaN(Half value, bool expected) + { + Assert.Equal(expected, Half.IsNaN(value)); + } + + public static IEnumerable IsNegative_TestData() + { + yield return new object[] { Half.NegativeInfinity, true }; // Negative Infinity + yield return new object[] { Half.MinValue, true }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), true }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), true }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), true }; // Max Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8000), true }; // Negative Zero + yield return new object[] { Half.NaN, true }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), false }; // Positive Zero + yield return new object[] { Half.Epsilon, false }; // Min Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x03FF), false }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), false }; // Min Positive Normal + yield return new object[] { Half.MaxValue, false }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, false }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsNegative_TestData))] + public static void IsNegative(Half value, bool expected) + { + Assert.Equal(expected, Half.IsNegative(value)); + } + + public static IEnumerable IsNegativeInfinity_TestData() + { + yield return new object[] { Half.NegativeInfinity, true }; // Negative Infinity + yield return new object[] { Half.MinValue, false }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), false }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), false }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), false }; // Max Negative Subnormal (Negative Epsilon) + yield return new object[] { UInt16BitsToHalf(0x8000), false }; // Negative Zero + yield return new object[] { Half.NaN, false }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), false }; // Positive Zero + yield return new object[] { Half.Epsilon, false }; // Min Positive Subnormal (Positive Epsilon) + yield return new object[] { UInt16BitsToHalf(0x03FF), false }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), false }; // Min Positive Normal + yield return new object[] { Half.MaxValue, false }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, false }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsNegativeInfinity_TestData))] + public static void IsNegativeInfinity(Half value, bool expected) + { + Assert.Equal(expected, Half.IsNegativeInfinity(value)); + } + + public static IEnumerable IsNormal_TestData() + { + yield return new object[] { Half.NegativeInfinity, false }; // Negative Infinity + yield return new object[] { Half.MinValue, true }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), true }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), false }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), false }; // Max Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8000), false }; // Negative Zero + yield return new object[] { Half.NaN, false }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), false }; // Positive Zero + yield return new object[] { Half.Epsilon, false }; // Min Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x03FF), false }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), true }; // Min Positive Normal + yield return new object[] { Half.MaxValue, true }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, false }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsNormal_TestData))] + public static void IsNormal(Half value, bool expected) + { + Assert.Equal(expected, Half.IsNormal(value)); + } + + public static IEnumerable IsPositiveInfinity_TestData() + { + yield return new object[] { Half.NegativeInfinity, false }; // Negative Infinity + yield return new object[] { Half.MinValue, false }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), false }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), false }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), false }; // Max Negative Subnormal (Negative Epsilon) + yield return new object[] { UInt16BitsToHalf(0x8000), false }; // Negative Zero + yield return new object[] { Half.NaN, false }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), false }; // Positive Zero + yield return new object[] { Half.Epsilon, false }; // Min Positive Subnormal (Positive Epsilon) + yield return new object[] { UInt16BitsToHalf(0x03FF), false }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), false }; // Min Positive Normal + yield return new object[] { Half.MaxValue, false }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, true }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsPositiveInfinity_TestData))] + public static void IsPositiveInfinity(Half value, bool expected) + { + Assert.Equal(expected, Half.IsPositiveInfinity(value)); + } + + public static IEnumerable IsSubnormal_TestData() + { + yield return new object[] { Half.NegativeInfinity, false }; // Negative Infinity + yield return new object[] { Half.MinValue, false }; // Min Negative Normal + yield return new object[] { UInt16BitsToHalf(0x8400), false }; // Max Negative Normal + yield return new object[] { UInt16BitsToHalf(0x83FF), true }; // Min Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8001), true }; // Max Negative Subnormal + yield return new object[] { UInt16BitsToHalf(0x8000), false }; // Negative Zero + yield return new object[] { Half.NaN, false }; // NaN + yield return new object[] { UInt16BitsToHalf(0x0000), false }; // Positive Zero + yield return new object[] { Half.Epsilon, true }; // Min Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x03FF), true }; // Max Positive Subnormal + yield return new object[] { UInt16BitsToHalf(0x0400), false }; // Min Positive Normal + yield return new object[] { Half.MaxValue, false }; // Max Positive Normal + yield return new object[] { Half.PositiveInfinity, false }; // Positive Infinity + } + + [Theory] + [MemberData(nameof(IsSubnormal_TestData))] + public static void IsSubnormal(Half value, bool expected) + { + Assert.Equal(expected, Half.IsSubnormal(value)); + } + + public static IEnumerable CompareTo_ThrowsArgumentException_TestData() + { + yield return new object[] { "a" }; + yield return new object[] { 234.0 }; + } + + [Theory] + [MemberData(nameof(CompareTo_ThrowsArgumentException_TestData))] + public static void CompareTo_ThrowsArgumentException(object obj) + { + Assert.Throws(() => Half.MaxValue.CompareTo(obj)); + } + + public static IEnumerable CompareTo_TestData() + { + yield return new object[] { Half.MaxValue, Half.MaxValue, 0 }; + yield return new object[] { Half.MaxValue, Half.MinValue, 1 }; + yield return new object[] { Half.Epsilon, UInt16BitsToHalf(0x8001), 1 }; + yield return new object[] { Half.MaxValue, UInt16BitsToHalf(0x0000), 1 }; + yield return new object[] { Half.MaxValue, Half.Epsilon, 1 }; + yield return new object[] { Half.MaxValue, Half.PositiveInfinity, -1 }; + yield return new object[] { Half.MinValue, Half.MaxValue, -1 }; + yield return new object[] { Half.MaxValue, Half.NaN, 1 }; + yield return new object[] { Half.NaN, Half.NaN, 0 }; + yield return new object[] { Half.NaN, UInt16BitsToHalf(0x0000), -1 }; + yield return new object[] { Half.MaxValue, null, 1 }; + } + + [Theory] + [MemberData(nameof(CompareTo_TestData))] + public static void CompareTo(Half value, object obj, int expected) + { + if (obj is Half other) + { + Assert.Equal(expected, Math.Sign(value.CompareTo(other))); + + if (Half.IsNaN(value) || Half.IsNaN(other)) + { + Assert.False(value >= other); + Assert.False(value > other); + Assert.False(value <= other); + Assert.False(value < other); + } + else + { + if (expected >= 0) + { + Assert.True(value >= other); + Assert.False(value < other); + } + if (expected > 0) + { + Assert.True(value > other); + Assert.False(value <= other); + } + if (expected <= 0) + { + Assert.True(value <= other); + Assert.False(value > other); + } + if (expected < 0) + { + Assert.True(value < other); + Assert.False(value >= other); + } + } + } + + Assert.Equal(expected, Math.Sign(value.CompareTo(obj))); + } + + public static IEnumerable Equals_TestData() + { + yield return new object[] { Half.MaxValue, Half.MaxValue, true }; + yield return new object[] { Half.MaxValue, Half.MinValue, false }; + yield return new object[] { Half.MaxValue, UInt16BitsToHalf(0x0000), false }; + yield return new object[] { Half.NaN, Half.NaN, false }; + yield return new object[] { Half.MaxValue, 789.0f, false }; + yield return new object[] { Half.MaxValue, "789", false }; + } + + [Theory] + [MemberData(nameof(Equals_TestData))] + public static void EqualsTest(Half value, object obj, bool expected) + { + Assert.Equal(expected, value.Equals(obj)); + } + + public static IEnumerable ExplicitConversion_ToSingle_TestData() + { + (Half Original, float Expected)[] data = // Fraction is shifted left by 42, Exponent is -15 then +127 = +112 + { + (UInt16BitsToHalf(0b0_01111_0000000000), 1f), // 1 + (UInt16BitsToHalf(0b1_01111_0000000000), -1f), // -1 + (Half.MaxValue, 65504f), // 65500 + (Half.MinValue, -65504f), // -65500 + (UInt16BitsToHalf(0b0_01011_1001100110), 0.0999755859375f), // 0.1ish + (UInt16BitsToHalf(0b1_01011_1001100110), -0.0999755859375f), // -0.1ish + (UInt16BitsToHalf(0b0_10100_0101000000), 42f), // 42 + (UInt16BitsToHalf(0b1_10100_0101000000), -42f), // -42 + (Half.PositiveInfinity, float.PositiveInfinity), // PosInfinity + (Half.NegativeInfinity, float.NegativeInfinity), // NegInfinity + (UInt16BitsToHalf(0b0_11111_1000000000), BitConverter.Int32BitsToSingle(0x7FC00000)), // Positive Quiet NaN + (Half.NaN, float.NaN), // Negative Quiet NaN + (UInt16BitsToHalf(0b0_11111_1010101010), BitConverter.Int32BitsToSingle(0x7FD54000)), // Positive Signalling NaN - Should preserve payload + (UInt16BitsToHalf(0b1_11111_1010101010), BitConverter.Int32BitsToSingle(unchecked((int)0xFFD54000))), // Negative Signalling NaN - Should preserve payload + (Half.Epsilon, 1/16777216f), // PosEpsilon = 0.000000059605... + (UInt16BitsToHalf(0), 0), // 0 + (UInt16BitsToHalf(0b1_00000_0000000000), -0f), // -0 + (UInt16BitsToHalf(0b0_10000_1001001000), 3.140625f), // 3.140625 + (UInt16BitsToHalf(0b1_10000_1001001000), -3.140625f), // -3.140625 + (UInt16BitsToHalf(0b0_10000_0101110000), 2.71875f), // 2.71875 + (UInt16BitsToHalf(0b1_10000_0101110000), -2.71875f), // -2.71875 + (UInt16BitsToHalf(0b0_01111_1000000000), 1.5f), // 1.5 + (UInt16BitsToHalf(0b1_01111_1000000000), -1.5f), // -1.5 + (UInt16BitsToHalf(0b0_01111_1000000001), 1.5009765625f), // 1.5009765625 + (UInt16BitsToHalf(0b1_01111_1000000001), -1.5009765625f), // -1.5009765625 + }; + + foreach ((Half original, float expected) in data) + { + yield return new object[] { original, expected }; + } + } + + [MemberData(nameof(ExplicitConversion_ToSingle_TestData))] + [Theory] + public static void ExplicitConversion_ToSingle(Half value, float expected) // Check the underlying bits for verifying NaNs + { + float f = (float)value; + Assert.Equal(BitConverter.SingleToInt32Bits(expected), BitConverter.SingleToInt32Bits(f)); + } + + public static IEnumerable ExplicitConversion_ToDouble_TestData() + { + (Half Original, double Expected)[] data = // Fraction is shifted left by 42, Exponent is -15 then +127 = +112 + { + (UInt16BitsToHalf(0b0_01111_0000000000), 1d), // 1 + (UInt16BitsToHalf(0b1_01111_0000000000), -1d), // -1 + (Half.MaxValue, 65504d), // 65500 + (Half.MinValue, -65504d), // -65500 + (UInt16BitsToHalf(0b0_01011_1001100110), 0.0999755859375d), // 0.1ish + (UInt16BitsToHalf(0b1_01011_1001100110), -0.0999755859375d), // -0.1ish + (UInt16BitsToHalf(0b0_10100_0101000000), 42d), // 42 + (UInt16BitsToHalf(0b1_10100_0101000000), -42d), // -42 + (Half.PositiveInfinity, double.PositiveInfinity), // PosInfinity + (Half.NegativeInfinity, double.NegativeInfinity), // NegInfinity + (UInt16BitsToHalf(0b0_11111_1000000000), BitConverter.Int64BitsToDouble(0x7FF80000_00000000)), // Positive Quiet NaN + (Half.NaN, double.NaN), // Negative Quiet NaN + (UInt16BitsToHalf(0b0_11111_1010101010), BitConverter.Int64BitsToDouble(0x7FFAA800_00000000)), // Positive Signalling NaN - Should preserve payload + (UInt16BitsToHalf(0b1_11111_1010101010), BitConverter.Int64BitsToDouble(unchecked((long)0xFFFAA800_00000000))), // Negative Signalling NaN - Should preserve payload + (Half.Epsilon, 1/16777216d), // PosEpsilon = 0.000000059605... + (UInt16BitsToHalf(0), 0d), // 0 + (UInt16BitsToHalf(0b1_00000_0000000000), -0d), // -0 + (UInt16BitsToHalf(0b0_10000_1001001000), 3.140625d), // 3.140625 + (UInt16BitsToHalf(0b1_10000_1001001000), -3.140625d), // -3.140625 + (UInt16BitsToHalf(0b0_10000_0101110000), 2.71875d), // 2.71875 + (UInt16BitsToHalf(0b1_10000_0101110000), -2.71875d), // -2.71875 + (UInt16BitsToHalf(0b0_01111_1000000000), 1.5d), // 1.5 + (UInt16BitsToHalf(0b1_01111_1000000000), -1.5d), // -1.5 + (UInt16BitsToHalf(0b0_01111_1000000001), 1.5009765625d), // 1.5009765625 + (UInt16BitsToHalf(0b1_01111_1000000001), -1.5009765625d) // -1.5009765625 + }; + + foreach ((Half original, double expected) in data) + { + yield return new object[] { original, expected }; + } + } + + [MemberData(nameof(ExplicitConversion_ToDouble_TestData))] + [Theory] + public static void ExplicitConversion_ToDouble(Half value, double expected) // Check the underlying bits for verifying NaNs + { + double d = (double)value; + Assert.Equal(BitConverter.DoubleToInt64Bits(expected), BitConverter.DoubleToInt64Bits(d)); + } + + // ---------- Start of To-half conversion tests ---------- + public static IEnumerable ExplicitConversion_FromSingle_TestData() + { + (float, Half)[] data = + { + (MathF.PI, UInt16BitsToHalf(0b0_10000_1001001000)), // 3.140625 + (MathF.E, UInt16BitsToHalf(0b0_10000_0101110000)), // 2.71875 + (-MathF.PI, UInt16BitsToHalf(0b1_10000_1001001000)), // -3.140625 + (-MathF.E, UInt16BitsToHalf(0b1_10000_0101110000)), // -2.71875 + (float.MaxValue, Half.PositiveInfinity), // Overflow + (float.MinValue, Half.NegativeInfinity), // Overflow + (float.PositiveInfinity, Half.PositiveInfinity), // Overflow + (float.NegativeInfinity, Half.NegativeInfinity), // Overflow + (float.NaN, Half.NaN), // Quiet Negative NaN + (BitConverter.Int32BitsToSingle(0x7FC00000), UInt16BitsToHalf(0b0_11111_1000000000)), // Quiet Positive NaN + (BitConverter.Int32BitsToSingle(unchecked((int)0xFFD55555)), + UInt16BitsToHalf(0b1_11111_1010101010)), // Signalling Negative NaN + (BitConverter.Int32BitsToSingle(0x7FD55555), UInt16BitsToHalf(0b0_11111_1010101010)), // Signalling Positive NaN + (float.Epsilon, UInt16BitsToHalf(0)), // Underflow + (-float.Epsilon, UInt16BitsToHalf(0b1_00000_0000000000)), // Underflow + (1f, UInt16BitsToHalf(0b0_01111_0000000000)), // 1 + (-1f, UInt16BitsToHalf(0b1_01111_0000000000)), // -1 + (0f, UInt16BitsToHalf(0)), // 0 + (-0f, UInt16BitsToHalf(0b1_00000_0000000000)), // -0 + (42f, UInt16BitsToHalf(0b0_10100_0101000000)), // 42 + (-42f, UInt16BitsToHalf(0b1_10100_0101000000)), // -42 + (0.1f, UInt16BitsToHalf(0b0_01011_1001100110)), // 0.0999755859375 + (-0.1f, UInt16BitsToHalf(0b1_01011_1001100110)), // -0.0999755859375 + (1.5f, UInt16BitsToHalf(0b0_01111_1000000000)), // 1.5 + (-1.5f, UInt16BitsToHalf(0b1_01111_1000000000)), // -1.5 + (1.5009765625f, UInt16BitsToHalf(0b0_01111_1000000001)), // 1.5009765625 + (-1.5009765625f, UInt16BitsToHalf(0b1_01111_1000000001)), // -1.5009765625 + }; + + foreach ((float original, Half expected) in data) + { + yield return new object[] { original, expected }; + } + } + + [MemberData(nameof(ExplicitConversion_FromSingle_TestData))] + [Theory] + public static void ExplicitConversion_FromSingle(float f, Half expected) // Check the underlying bits for verifying NaNs + { + Half h = (Half)f; + Assert.Equal(HalfToUInt16Bits(expected), HalfToUInt16Bits(h)); + } + + public static IEnumerable ExplicitConversion_FromDouble_TestData() + { + (double, Half)[] data = + { + (Math.PI, UInt16BitsToHalf(0b0_10000_1001001000)), // 3.140625 + (Math.E, UInt16BitsToHalf(0b0_10000_0101110000)), // 2.71875 + (-Math.PI, UInt16BitsToHalf(0b1_10000_1001001000)), // -3.140625 + (-Math.E, UInt16BitsToHalf(0b1_10000_0101110000)), // -2.71875 + (double.MaxValue, Half.PositiveInfinity), // Overflow + (double.MinValue, Half.NegativeInfinity), // Overflow + (double.PositiveInfinity, Half.PositiveInfinity), // Overflow + (double.NegativeInfinity, Half.NegativeInfinity), // Overflow + (double.NaN, Half.NaN), // Quiet Negative NaN + (BitConverter.Int64BitsToDouble(0x7FF80000_00000000), + UInt16BitsToHalf(0b0_11111_1000000000)), // Quiet Positive NaN + (BitConverter.Int64BitsToDouble(unchecked((long)0xFFFAAAAA_AAAAAAAA)), + UInt16BitsToHalf(0b1_11111_1010101010)), // Signalling Negative NaN + (BitConverter.Int64BitsToDouble(0x7FFAAAAA_AAAAAAAA), + UInt16BitsToHalf(0b0_11111_1010101010)), // Signalling Positive NaN + (double.Epsilon, UInt16BitsToHalf(0)), // Underflow + (-double.Epsilon, UInt16BitsToHalf(0b1_00000_0000000000)), // Underflow + (1d, UInt16BitsToHalf(0b0_01111_0000000000)), // 1 + (-1d, UInt16BitsToHalf(0b1_01111_0000000000)), // -1 + (0d, UInt16BitsToHalf(0)), // 0 + (-0d, UInt16BitsToHalf(0b1_00000_0000000000)), // -0 + (42d, UInt16BitsToHalf(0b0_10100_0101000000)), // 42 + (-42d, UInt16BitsToHalf(0b1_10100_0101000000)), // -42 + (0.1d, UInt16BitsToHalf(0b0_01011_1001100110)), // 0.0999755859375 + (-0.1d, UInt16BitsToHalf(0b1_01011_1001100110)), // -0.0999755859375 + (1.5d, UInt16BitsToHalf(0b0_01111_1000000000)), // 1.5 + (-1.5d, UInt16BitsToHalf(0b1_01111_1000000000)), // -1.5 + (1.5009765625d, UInt16BitsToHalf(0b0_01111_1000000001)), // 1.5009765625 + (-1.5009765625d, UInt16BitsToHalf(0b1_01111_1000000001)), // -1.5009765625 + }; + + foreach ((double original, Half expected) in data) + { + yield return new object[] { original, expected }; + } + } + + [MemberData(nameof(ExplicitConversion_FromDouble_TestData))] + [Theory] + public static void ExplicitConversion_FromDouble(double d, Half expected) // Check the underlying bits for verifying NaNs + { + Half h = (Half)d; + Assert.Equal(HalfToUInt16Bits(expected), HalfToUInt16Bits(h)); + } + + public static IEnumerable Parse_Valid_TestData() + { + NumberStyles defaultStyle = NumberStyles.Float | NumberStyles.AllowThousands; + + NumberFormatInfo emptyFormat = NumberFormatInfo.CurrentInfo; + + var dollarSignCommaSeparatorFormat = new NumberFormatInfo() + { + CurrencySymbol = "$", + CurrencyGroupSeparator = "," + }; + + var decimalSeparatorFormat = new NumberFormatInfo() + { + NumberDecimalSeparator = "." + }; + + NumberFormatInfo invariantFormat = NumberFormatInfo.InvariantInfo; + + yield return new object[] { "-123", defaultStyle, null, -123.0f }; + yield return new object[] { "0", defaultStyle, null, 0.0f }; + yield return new object[] { "123", defaultStyle, null, 123.0f }; + yield return new object[] { " 123 ", defaultStyle, null, 123.0f }; + yield return new object[] { (567.89f).ToString(), defaultStyle, null, 567.89f }; + yield return new object[] { (-567.89f).ToString(), defaultStyle, null, -567.89f }; + yield return new object[] { "1E23", defaultStyle, null, 1E23f }; + + yield return new object[] { (123.1f).ToString(), NumberStyles.AllowDecimalPoint, null, 123.1f }; + yield return new object[] { (1000.0f).ToString("N0"), NumberStyles.AllowThousands, null, 1000.0f }; + + yield return new object[] { "123", NumberStyles.Any, emptyFormat, 123.0f }; + yield return new object[] { (123.567f).ToString(), NumberStyles.Any, emptyFormat, 123.567f }; + yield return new object[] { "123", NumberStyles.Float, emptyFormat, 123.0f }; + yield return new object[] { "$1,000", NumberStyles.Currency, dollarSignCommaSeparatorFormat, 1000.0f }; + yield return new object[] { "$1000", NumberStyles.Currency, dollarSignCommaSeparatorFormat, 1000.0f }; + yield return new object[] { "123.123", NumberStyles.Float, decimalSeparatorFormat, 123.123f }; + yield return new object[] { "(123)", NumberStyles.AllowParentheses, decimalSeparatorFormat, -123.0f }; + + yield return new object[] { "NaN", NumberStyles.Any, invariantFormat, float.NaN }; + yield return new object[] { "Infinity", NumberStyles.Any, invariantFormat, float.PositiveInfinity }; + yield return new object[] { "-Infinity", NumberStyles.Any, invariantFormat, float.NegativeInfinity }; + } + + [Theory] + [MemberData(nameof(Parse_Valid_TestData))] + public static void Parse(string value, NumberStyles style, IFormatProvider provider, float expectedFloat) + { + bool isDefaultProvider = provider == null || provider == NumberFormatInfo.CurrentInfo; + Half result; + Half expected = (Half)expectedFloat; + if ((style & ~(NumberStyles.Float | NumberStyles.AllowThousands)) == 0 && style != NumberStyles.None) + { + // Use Parse(string) or Parse(string, IFormatProvider) + if (isDefaultProvider) + { + Assert.True(Half.TryParse(value, out result)); + Assert.True(expected.Equals(result)); + + Assert.Equal(expected, Half.Parse(value)); + } + + Assert.True(expected.Equals(Half.Parse(value, provider: provider))); + } + + // Use Parse(string, NumberStyles, IFormatProvider) + Assert.True(Half.TryParse(value, style, provider, out result)); + Assert.True(expected.Equals(result) || (Half.IsNaN(expected) && Half.IsNaN(result))); + + Assert.True(expected.Equals(Half.Parse(value, style, provider)) || (Half.IsNaN(expected) && Half.IsNaN(result))); + + if (isDefaultProvider) + { + // Use Parse(string, NumberStyles) or Parse(string, NumberStyles, IFormatProvider) + Assert.True(Half.TryParse(value, style, NumberFormatInfo.CurrentInfo, out result)); + Assert.True(expected.Equals(result)); + + Assert.True(expected.Equals(Half.Parse(value, style))); + Assert.True(expected.Equals(Half.Parse(value, style, NumberFormatInfo.CurrentInfo))); + } + } + + public static IEnumerable Parse_Invalid_TestData() + { + NumberStyles defaultStyle = NumberStyles.Float; + + var dollarSignDecimalSeparatorFormat = new NumberFormatInfo(); + dollarSignDecimalSeparatorFormat.CurrencySymbol = "$"; + dollarSignDecimalSeparatorFormat.NumberDecimalSeparator = "."; + + yield return new object[] { null, defaultStyle, null, typeof(ArgumentNullException) }; + yield return new object[] { "", defaultStyle, null, typeof(FormatException) }; + yield return new object[] { " ", defaultStyle, null, typeof(FormatException) }; + yield return new object[] { "Garbage", defaultStyle, null, typeof(FormatException) }; + + yield return new object[] { "ab", defaultStyle, null, typeof(FormatException) }; // Hex value + yield return new object[] { "(123)", defaultStyle, null, typeof(FormatException) }; // Parentheses + yield return new object[] { (100.0f).ToString("C0"), defaultStyle, null, typeof(FormatException) }; // Currency + + yield return new object[] { (123.456f).ToString(), NumberStyles.Integer, null, typeof(FormatException) }; // Decimal + yield return new object[] { " " + (123.456f).ToString(), NumberStyles.None, null, typeof(FormatException) }; // Leading space + yield return new object[] { (123.456f).ToString() + " ", NumberStyles.None, null, typeof(FormatException) }; // Leading space + yield return new object[] { "1E23", NumberStyles.None, null, typeof(FormatException) }; // Exponent + + yield return new object[] { "ab", NumberStyles.None, null, typeof(FormatException) }; // Negative hex value + yield return new object[] { " 123 ", NumberStyles.None, null, typeof(FormatException) }; // Trailing and leading whitespace + } + + [Theory] + [MemberData(nameof(Parse_Invalid_TestData))] + public static void Parse_Invalid(string value, NumberStyles style, IFormatProvider provider, Type exceptionType) + { + bool isDefaultProvider = provider == null || provider == NumberFormatInfo.CurrentInfo; + Half result; + if ((style & ~(NumberStyles.Float | NumberStyles.AllowThousands)) == 0 && style != NumberStyles.None && (style & NumberStyles.AllowLeadingWhite) == (style & NumberStyles.AllowTrailingWhite)) + { + // Use Parse(string) or Parse(string, IFormatProvider) + if (isDefaultProvider) + { + Assert.False(Half.TryParse(value, out result)); + Assert.Equal(default(Half), result); + + Assert.Throws(exceptionType, () => Half.Parse(value)); + } + + Assert.Throws(exceptionType, () => Half.Parse(value, provider: provider)); + } + + // Use Parse(string, NumberStyles, IFormatProvider) + Assert.False(Half.TryParse(value, style, provider, out result)); + Assert.Equal(default(Half), result); + + Assert.Throws(exceptionType, () => Half.Parse(value, style, provider)); + + if (isDefaultProvider) + { + // Use Parse(string, NumberStyles) or Parse(string, NumberStyles, IFormatProvider) + Assert.False(Half.TryParse(value, style, NumberFormatInfo.CurrentInfo, out result)); + Assert.Equal(default(Half), result); + + Assert.Throws(exceptionType, () => Half.Parse(value, style)); + Assert.Throws(exceptionType, () => Half.Parse(value, style, NumberFormatInfo.CurrentInfo)); + } + } + + public static IEnumerable ToString_TestData() + { + yield return new object[] { -4570.0f, "G", null, "-4570" }; + yield return new object[] { 0.0f, "G", null, "0" }; + yield return new object[] { 4570.0f, "G", null, "4570" }; + + yield return new object[] { float.NaN, "G", null, "NaN" }; + + yield return new object[] { 2468.0f, "N", null, "2,468.00" }; + + // Changing the negative pattern doesn't do anything without also passing in a format string + var customNegativePattern = new NumberFormatInfo() { NumberNegativePattern = 0 }; + yield return new object[] { -6310.0f, "G", customNegativePattern, "-6310" }; + + var customNegativeSignDecimalGroupSeparator = new NumberFormatInfo() + { + NegativeSign = "#", + NumberDecimalSeparator = "~", + NumberGroupSeparator = "*" + }; + yield return new object[] { -2468.0f, "N", customNegativeSignDecimalGroupSeparator, "#2*468~00" }; + yield return new object[] { 2468.0f, "N", customNegativeSignDecimalGroupSeparator, "2*468~00" }; + + var customNegativeSignGroupSeparatorNegativePattern = new NumberFormatInfo() + { + NegativeSign = "xx", // Set to trash to make sure it doesn't show up + NumberGroupSeparator = "*", + NumberNegativePattern = 0 + }; + yield return new object[] { -2468.0f, "N", customNegativeSignGroupSeparatorNegativePattern, "(2*468.00)" }; + + NumberFormatInfo invariantFormat = NumberFormatInfo.InvariantInfo; + yield return new object[] { float.NaN, "G", invariantFormat, "NaN" }; + yield return new object[] { float.PositiveInfinity, "G", invariantFormat, "Infinity" }; + yield return new object[] { float.NegativeInfinity, "G", invariantFormat, "-Infinity" }; + } + + public static IEnumerable ToString_TestData_NotNetFramework() + { + foreach (var testData in ToString_TestData()) + { + yield return testData; + } + + yield return new object[] { Half.MinValue, "G", null, "-65500" }; + yield return new object[] { Half.MaxValue, "G", null, "65500" }; + + yield return new object[] { Half.Epsilon, "G", null, "6E-08" }; + + NumberFormatInfo invariantFormat = NumberFormatInfo.InvariantInfo; + yield return new object[] { Half.Epsilon, "G", invariantFormat, "6E-08" }; + } + + [Fact] + public static void Test_ToString_NotNetFramework() + { + using (new ThreadCultureChange(CultureInfo.InvariantCulture)) + { + foreach (object[] testdata in ToString_TestData_NotNetFramework()) + { + ToStringTest(testdata[0] is float floatData ? (Half)floatData : (Half)testdata[0], (string)testdata[1], (IFormatProvider)testdata[2], (string)testdata[3]); + } + } + } + + private static void ToStringTest(Half f, string format, IFormatProvider provider, string expected) + { + bool isDefaultProvider = provider == null; + if (string.IsNullOrEmpty(format) || format.ToUpperInvariant() == "G") + { + if (isDefaultProvider) + { + Assert.Equal(expected, f.ToString()); + Assert.Equal(expected, f.ToString((IFormatProvider)null)); + } + Assert.Equal(expected, f.ToString(provider)); + } + if (isDefaultProvider) + { + Assert.Equal(expected.Replace('e', 'E'), f.ToString(format.ToUpperInvariant())); // If format is upper case, then exponents are printed in upper case + Assert.Equal(expected.Replace('E', 'e'), f.ToString(format.ToLowerInvariant())); // If format is lower case, then exponents are printed in lower case + Assert.Equal(expected.Replace('e', 'E'), f.ToString(format.ToUpperInvariant(), null)); + Assert.Equal(expected.Replace('E', 'e'), f.ToString(format.ToLowerInvariant(), null)); + } + Assert.Equal(expected.Replace('e', 'E'), f.ToString(format.ToUpperInvariant(), provider)); + Assert.Equal(expected.Replace('E', 'e'), f.ToString(format.ToLowerInvariant(), provider)); + } + + public static IEnumerable Parse_ValidWithOffsetCount_TestData() + { + foreach (object[] inputs in Parse_Valid_TestData()) + { + yield return new object[] { inputs[0], 0, ((string)inputs[0]).Length, inputs[1], inputs[2], inputs[3] }; + } + + const NumberStyles DefaultStyle = NumberStyles.Float | NumberStyles.AllowThousands; + + yield return new object[] { "-123", 1, 3, DefaultStyle, null, (float)123 }; + yield return new object[] { "-123", 0, 3, DefaultStyle, null, (float)-12 }; + yield return new object[] { "1E23", 0, 3, DefaultStyle, null, (float)1E2 }; + yield return new object[] { "123", 0, 2, NumberStyles.Float, new NumberFormatInfo(), (float)12 }; + yield return new object[] { "$1,000", 1, 3, NumberStyles.Currency, new NumberFormatInfo() { CurrencySymbol = "$", CurrencyGroupSeparator = "," }, (float)10 }; + yield return new object[] { "(123)", 1, 3, NumberStyles.AllowParentheses, new NumberFormatInfo() { NumberDecimalSeparator = "." }, (float)123 }; + yield return new object[] { "-Infinity", 1, 8, NumberStyles.Any, NumberFormatInfo.InvariantInfo, float.PositiveInfinity }; + } + + [Theory] + [MemberData(nameof(Parse_ValidWithOffsetCount_TestData))] + public static void Parse_Span_Valid(string value, int offset, int count, NumberStyles style, IFormatProvider provider, float expectedFloat) + { + bool isDefaultProvider = provider == null || provider == NumberFormatInfo.CurrentInfo; + Half result; + Half expected = (Half)expectedFloat; + if ((style & ~(NumberStyles.Float | NumberStyles.AllowThousands)) == 0 && style != NumberStyles.None) + { + // Use Parse(string) or Parse(string, IFormatProvider) + if (isDefaultProvider) + { + Assert.True(Half.TryParse(value.AsSpan(offset, count), out result)); + Assert.Equal(expected, result); + + Assert.Equal(expected, Half.Parse(value.AsSpan(offset, count))); + } + + Assert.Equal(expected, Half.Parse(value.AsSpan(offset, count), provider: provider)); + } + + Assert.True(expected.Equals(Half.Parse(value.AsSpan(offset, count), style, provider)) || (Half.IsNaN(expected) && Half.IsNaN(Half.Parse(value.AsSpan(offset, count), style, provider)))); + + Assert.True(Half.TryParse(value.AsSpan(offset, count), style, provider, out result)); + Assert.True(expected.Equals(result) || (Half.IsNaN(expected) && Half.IsNaN(result))); + } + + [Theory] + [MemberData(nameof(Parse_Invalid_TestData))] + public static void Parse_Span_Invalid(string value, NumberStyles style, IFormatProvider provider, Type exceptionType) + { + if (value != null) + { + Assert.Throws(exceptionType, () => float.Parse(value.AsSpan(), style, provider)); + + Assert.False(float.TryParse(value.AsSpan(), style, provider, out float result)); + Assert.Equal(0, result); + } + } + + [Fact] + public static void TryFormat() + { + using (new ThreadCultureChange(CultureInfo.InvariantCulture)) + { + foreach (object[] testdata in ToString_TestData()) + { + float localI = (float)testdata[0]; + string localFormat = (string)testdata[1]; + IFormatProvider localProvider = (IFormatProvider)testdata[2]; + string localExpected = (string)testdata[3]; + + try + { + char[] actual; + int charsWritten; + + // Just right + actual = new char[localExpected.Length]; + Assert.True(localI.TryFormat(actual.AsSpan(), out charsWritten, localFormat, localProvider)); + Assert.Equal(localExpected.Length, charsWritten); + Assert.Equal(localExpected, new string(actual)); + + // Longer than needed + actual = new char[localExpected.Length + 1]; + Assert.True(localI.TryFormat(actual.AsSpan(), out charsWritten, localFormat, localProvider)); + Assert.Equal(localExpected.Length, charsWritten); + Assert.Equal(localExpected, new string(actual, 0, charsWritten)); + + // Too short + if (localExpected.Length > 0) + { + actual = new char[localExpected.Length - 1]; + Assert.False(localI.TryFormat(actual.AsSpan(), out charsWritten, localFormat, localProvider)); + Assert.Equal(0, charsWritten); + } + } + catch (Exception exc) + { + throw new Exception($"Failed on `{localI}`, `{localFormat}`, `{localProvider}`, `{localExpected}`. {exc}"); + } + } + } + } + + public static IEnumerable ToStringRoundtrip_TestData() + { + yield return new object[] { Half.NegativeInfinity }; + yield return new object[] { Half.MinValue }; + yield return new object[] { -MathF.PI }; + yield return new object[] { -MathF.E }; + yield return new object[] { -0.845512408f }; + yield return new object[] { -0.0f }; + yield return new object[] { Half.NaN }; + yield return new object[] { 0.0f }; + yield return new object[] { 0.845512408f }; + yield return new object[] { Half.Epsilon }; + yield return new object[] { MathF.E }; + yield return new object[] { MathF.PI }; + yield return new object[] { Half.MaxValue }; + yield return new object[] { Half.PositiveInfinity }; + } + + [Theory] + [MemberData(nameof(ToStringRoundtrip_TestData))] + public static void ToStringRoundtrip(object o_value) + { + float value = o_value is float floatValue ? floatValue : (float)(Half)o_value; + Half result = Half.Parse(value.ToString()); + Assert.Equal(HalfToUInt16Bits((Half)value), HalfToUInt16Bits(result)); + } + + [Theory] + [MemberData(nameof(ToStringRoundtrip_TestData))] + public static void ToStringRoundtrip_R(object o_value) + { + float value = o_value is float floatValue ? floatValue : (float)(Half)o_value; + Half result = Half.Parse(value.ToString("R")); + Assert.Equal(HalfToUInt16Bits((Half)value), HalfToUInt16Bits(result)); + } + + public static IEnumerable RoundTripFloat_CornerCases() + { + // Magnitude smaller than 2^-24 maps to 0 + yield return new object[] { (Half)(5.2e-20f), 0 }; + yield return new object[] { (Half)(-5.2e-20f), 0 }; + // Magnitude smaller than 2^(map to subnormals + yield return new object[] { (Half)(1.52e-5f), 1.52e-5f }; + yield return new object[] { (Half)(-1.52e-5f), -1.52e-5f }; + // Normal numbers + yield return new object[] { (Half)(55.77f), 55.75f }; + yield return new object[] { (Half)(-55.77f), -55.75f }; + // Magnitude smaller than 2^(map to infinity + yield return new object[] { (Half)(1.7e38f), float.PositiveInfinity }; + yield return new object[] { (Half)(-1.7e38f), float.NegativeInfinity }; + // Infinity and NaN map to infinity and Nan + yield return new object[] { Half.PositiveInfinity, float.PositiveInfinity }; + yield return new object[] { Half.NegativeInfinity, float.NegativeInfinity }; + yield return new object[] { Half.NaN, float.NaN }; + } + + [Theory] + [MemberData(nameof(RoundTripFloat_CornerCases))] + public static void ToSingle(Half half, float verify) + { + float f = (float)half; + Assert.Equal(f, verify, precision: 1); + } + } +}