Implement IUtf8SpanFormattable on Guid (#84553)
authorStephen Toub <stoub@microsoft.com>
Sun, 16 Apr 2023 14:31:20 +0000 (10:31 -0400)
committerGitHub <noreply@github.com>
Sun, 16 Apr 2023 14:31:20 +0000 (10:31 -0400)
src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Guid.cs
src/libraries/System.Private.CoreLib/src/System/Guid.cs
src/libraries/System.Runtime/ref/System.Runtime.cs
src/libraries/System.Runtime/tests/System/GuidTests.cs

index 8effbc24857b01c9a7a6ffb17ad3540b4d933bf7..3e64fbd3ec754b30b6c2ead08e37c029c8bb166f 100644 (file)
@@ -1,83 +1,10 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using System.Runtime.Intrinsics;
-using System.Runtime.Intrinsics.Arm;
-using System.Runtime.Intrinsics.X86;
-
 namespace System.Buffers.Text
 {
     public static partial class Utf8Formatter
     {
-        #region Constants
-
-        private const byte OpenBrace = (byte)'{';
-        private const byte CloseBrace = (byte)'}';
-
-        private const byte OpenParen = (byte)'(';
-        private const byte CloseParen = (byte)')';
-
-        private const byte Dash = (byte)'-';
-
-        #endregion Constants
-
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        internal static (Vector128<byte>, Vector128<byte>, Vector128<byte>) FormatGuidVector128Utf8(Guid value, bool useDashes)
-        {
-            Debug.Assert((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian);
-            // Vectorized implementation for D, N, P and B formats:
-            // [{|(]dddddddd[-]dddd[-]dddd[-]dddd[-]dddddddddddd[}|)]
-
-            Vector128<byte> hexMap = Vector128.Create(
-                (byte)'0', (byte)'1', (byte)'2', (byte)'3',
-                (byte)'4', (byte)'5', (byte)'6', (byte)'7',
-                (byte)'8', (byte)'9', (byte)'a', (byte)'b',
-                (byte)'c', (byte)'d', (byte)'e', (byte)'f');
-
-            Vector128<byte> srcVec = Unsafe.As<Guid, Vector128<byte>>(ref value);
-            (Vector128<byte> hexLow, Vector128<byte> hexHigh) =
-                HexConverter.AsciiToHexVector128(srcVec, hexMap);
-
-            // because of Guid's layout (int _a, short _b, _c, <8 byte fields>)
-            // we have to shuffle some bytes for _a, _b and _c
-            hexLow = Vector128.Shuffle(hexLow.AsInt16(), Vector128.Create(3, 2, 1, 0, 5, 4, 7, 6)).AsByte();
-
-            if (useDashes)
-            {
-                // We divide 16 bytes into 3 x Vector128<byte>:
-                //
-                // ________-____-____-____-____________
-                // xxxxxxxxxxxxxxxx
-                //                     yyyyyyyyyyyyyyyy
-                //         zzzzzzzzzzzzzzzz
-                //
-                // Vector "x" - just one dash, shift all elements after it.
-                Vector128<byte> vecX = Vector128.Shuffle(hexLow,
-                    Vector128.Create(0x706050403020100, 0xD0CFF0B0A0908FF).AsByte());
-
-                // Vector "y" - same here.
-                Vector128<byte> vecY = Vector128.Shuffle(hexHigh,
-                    Vector128.Create(0x7060504FF030201, 0xF0E0D0C0B0A0908).AsByte());
-
-                // Vector "z" - we need to merge some elements of hexLow with hexHigh and add 4 dashes.
-                Vector128<byte> mid1 = Vector128.Shuffle(hexLow,
-                    Vector128.Create(0x0D0CFF0B0A0908FF, 0xFFFFFFFFFFFF0F0E).AsByte());
-                Vector128<byte> mid2 = Vector128.Shuffle(hexHigh,
-                    Vector128.Create(0xFFFFFFFFFFFFFFFF, 0xFF03020100FFFFFF).AsByte());
-                Vector128<byte> dashesMask = Vector128.Shuffle(Vector128.CreateScalarUnsafe((byte)'-'),
-                    Vector128.Create(0xFFFF00FFFFFFFF00, 0x00FFFFFFFF00FFFF).AsByte());
-
-                Vector128<byte> vecZ = (mid1 | mid2 | dashesMask);
-                return (vecX, vecY, vecZ);
-            }
-
-            // N format - no dashes.
-            return (hexLow, hexHigh, default);
-        }
-
         /// <summary>
         /// Formats a Guid as a UTF8 string.
         /// </summary>
@@ -101,41 +28,24 @@ namespace System.Buffers.Text
         /// </exceptions>
         public static bool TryFormat(Guid value, Span<byte> destination, out int bytesWritten, StandardFormat format = default)
         {
-            const int INSERT_DASHES = unchecked((int)0x80000000);
-            const int NO_DASHES = 0;
-            const int INSERT_CURLY_BRACES = (CloseBrace << 16) | (OpenBrace << 8);
-            const int INSERT_ROUND_BRACES = (CloseParen << 16) | (OpenParen << 8);
-            const int NO_BRACES = 0;
-            const int LEN_GUID_BASE = 32;
-            const int LEN_ADD_DASHES = 4;
-            const int LEN_ADD_BRACES = 2;
-
-            // This is a 32-bit value whose contents (where 0 is the low byte) are:
-            // 0th byte: minimum required length of the output buffer,
-            // 1st byte: the ASCII byte to insert for the opening brace position (or 0 if no braces),
-            // 2nd byte: the ASCII byte to insert for the closing brace position (or 0 if no braces),
-            // 3rd byte: high bit set if dashes are to be inserted.
-            //
-            // The reason for keeping a single flag instead of separate vars is that we can avoid register spillage
-            // as we build up the output value.
             int flags;
 
             switch (FormattingHelpers.GetSymbolOrDefault(format, 'D'))
             {
                 case 'D': // nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn
-                    flags = INSERT_DASHES + NO_BRACES + LEN_GUID_BASE + LEN_ADD_DASHES;
+                    flags = 36 + Guid.TryFormatFlags_UseDashes;
                     break;
 
                 case 'B': // {nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn}
-                    flags = INSERT_DASHES + INSERT_CURLY_BRACES + LEN_GUID_BASE + LEN_ADD_DASHES + LEN_ADD_BRACES;
+                    flags = 38 + Guid.TryFormatFlags_UseDashes + Guid.TryFormatFlags_CurlyBraces;
                     break;
 
                 case 'P': // (nnnnnnnn-nnnn-nnnn-nnnn-nnnnnnnnnnnn)
-                    flags = INSERT_DASHES + INSERT_ROUND_BRACES + LEN_GUID_BASE + LEN_ADD_DASHES + LEN_ADD_BRACES;
+                    flags = 38 + Guid.TryFormatFlags_UseDashes + Guid.TryFormatFlags_Parens;
                     break;
 
                 case 'N': // nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn
-                    flags = NO_BRACES + NO_DASHES + LEN_GUID_BASE;
+                    flags = 32;
                     break;
 
                 default:
@@ -143,191 +53,7 @@ namespace System.Buffers.Text
                     goto case 'D'; // unreachable
             }
 
-            // At this point, the low byte of flags contains the minimum required length
-
-            if ((byte)flags > destination.Length)
-            {
-                bytesWritten = 0;
-                return false;
-            }
-
-            bytesWritten = (byte)flags;
-            flags >>= 8;
-
-            // At this point, the low byte of flags contains the opening brace char (if any)
-
-            if ((byte)flags != 0)
-            {
-                destination[0] = (byte)flags;
-                destination = destination.Slice(1);
-            }
-            flags >>= 8;
-
-            if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian)
-            {
-                // Vectorized implementation for D, N, P and B formats:
-                // [{|(]dddddddd[-]dddd[-]dddd[-]dddd[-]dddddddddddd[}|)]
-                (Vector128<byte> vecX, Vector128<byte> vecY, Vector128<byte> vecZ) =
-                    FormatGuidVector128Utf8(value, flags < 0 /*dash*/);
-
-                ref byte dest = ref MemoryMarshal.GetReference(destination);
-                if (flags < 0)
-                {
-                    // We need to merge these vectors in this order:
-                    // xxxxxxxxxxxxxxxx
-                    //                     yyyyyyyyyyyyyyyy
-                    //         zzzzzzzzzzzzzzzz
-                    vecX.StoreUnsafe(ref dest, 0);
-                    vecY.StoreUnsafe(ref dest, 20);
-                    vecZ.StoreUnsafe(ref dest, 8);
-                }
-                else
-                {
-                    // xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyy
-                    vecX.StoreUnsafe(ref dest, 0);
-                    vecY.StoreUnsafe(ref dest, 16);
-                }
-                if ((byte)flags != 0)
-                    Unsafe.Add(ref dest, flags < 0 ? 36 : 32) = (byte)flags;
-                return true;
-            }
-
-            // At this point, the low byte of flags contains the closing brace char (if any)
-            // And since we're performing arithmetic shifting the high bit of flags is set (flags is negative) if dashes are required
-
-            DecomposedGuid guidAsBytes = default;
-            guidAsBytes.Guid = value;
-
-            // When a GUID is blitted, the first three components are native-endian, and the last component is big-endian.
-
-            // The line below forces the JIT to hoist the bounds check for the following segment.
-            // The JIT will optimize away the read, but it cannot optimize away the bounds check
-            // because it may have an observable side effect (throwing).
-            // We use 8 instead of 7 so that we also capture the dash if we're asked to insert one.
-
-            { _ = destination[8]; }
-            if (BitConverter.IsLittleEndian)
-            {
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte03, destination, 0, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte02, destination, 2, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte01, destination, 4, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte00, destination, 6, HexConverter.Casing.Lower);
-            }
-            else
-            {
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte00, destination, 0, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte01, destination, 2, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte02, destination, 4, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte03, destination, 6, HexConverter.Casing.Lower);
-            }
-
-            if (flags < 0 /* use dash? */)
-            {
-                destination[8] = Dash;
-                destination = destination.Slice(9);
-            }
-            else
-            {
-                destination = destination.Slice(8);
-            }
-
-            { _ = destination[4]; }
-            if (BitConverter.IsLittleEndian)
-            {
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte05, destination, 0, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte04, destination, 2, HexConverter.Casing.Lower);
-            }
-            else
-            {
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte04, destination, 0, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte05, destination, 2, HexConverter.Casing.Lower);
-            }
-
-            if (flags < 0 /* use dash? */)
-            {
-                destination[4] = Dash;
-                destination = destination.Slice(5);
-            }
-            else
-            {
-                destination = destination.Slice(4);
-            }
-
-            { _ = destination[4]; }
-            if (BitConverter.IsLittleEndian)
-            {
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte07, destination, 0, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte06, destination, 2, HexConverter.Casing.Lower);
-            }
-            else
-            {
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte06, destination, 0, HexConverter.Casing.Lower);
-                HexConverter.ToBytesBuffer(guidAsBytes.Byte07, destination, 2, HexConverter.Casing.Lower);
-            }
-
-            if (flags < 0 /* use dash? */)
-            {
-                destination[4] = Dash;
-                destination = destination.Slice(5);
-            }
-            else
-            {
-                destination = destination.Slice(4);
-            }
-
-            { _ = destination[4]; }
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte08, destination, 0, HexConverter.Casing.Lower);
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte09, destination, 2, HexConverter.Casing.Lower);
-
-            if (flags < 0 /* use dash? */)
-            {
-                destination[4] = Dash;
-                destination = destination.Slice(5);
-            }
-            else
-            {
-                destination = destination.Slice(4);
-            }
-
-            { _ = destination[11]; } // can't hoist bounds check on the final brace (if exists)
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte10, destination, 0, HexConverter.Casing.Lower);
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte11, destination, 2, HexConverter.Casing.Lower);
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte12, destination, 4, HexConverter.Casing.Lower);
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte13, destination, 6, HexConverter.Casing.Lower);
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte14, destination, 8, HexConverter.Casing.Lower);
-            HexConverter.ToBytesBuffer(guidAsBytes.Byte15, destination, 10, HexConverter.Casing.Lower);
-
-            if ((byte)flags != 0)
-            {
-                destination[12] = (byte)flags;
-            }
-
-            return true;
-        }
-
-        /// <summary>
-        /// Used to provide access to the individual bytes of a GUID.
-        /// </summary>
-        [StructLayout(LayoutKind.Explicit)]
-        private struct DecomposedGuid
-        {
-            [FieldOffset(00)] public Guid Guid;
-            [FieldOffset(00)] public byte Byte00;
-            [FieldOffset(01)] public byte Byte01;
-            [FieldOffset(02)] public byte Byte02;
-            [FieldOffset(03)] public byte Byte03;
-            [FieldOffset(04)] public byte Byte04;
-            [FieldOffset(05)] public byte Byte05;
-            [FieldOffset(06)] public byte Byte06;
-            [FieldOffset(07)] public byte Byte07;
-            [FieldOffset(08)] public byte Byte08;
-            [FieldOffset(09)] public byte Byte09;
-            [FieldOffset(10)] public byte Byte10;
-            [FieldOffset(11)] public byte Byte11;
-            [FieldOffset(12)] public byte Byte12;
-            [FieldOffset(13)] public byte Byte13;
-            [FieldOffset(14)] public byte Byte14;
-            [FieldOffset(15)] public byte Byte15;
+            return value.TryFormatCore(destination, out bytesWritten, flags);
         }
     }
 }
index 4a1ede42b511ac2c56adadf3f3deef6dbe02deef..ec28fdeb45aaac63cddba09c970c43aa898dc7fc 100644 (file)
@@ -25,7 +25,8 @@ namespace System
           IComparable,
           IComparable<Guid>,
           IEquatable<Guid>,
-          ISpanParsable<Guid>
+          ISpanParsable<Guid>,
+          IUtf8SpanFormattable
     {
         public static readonly Guid Empty;
 
@@ -306,7 +307,7 @@ namespace System
             if (format.Length != 1)
             {
                 // all acceptable format strings are of length 1
-                throw new FormatException(SR.Format_InvalidGuidFormatSpecification);
+                ThrowBadGuidFormatSpecification();
             }
 
             input = input.Trim();
@@ -883,9 +884,6 @@ namespace System
             return true;
         }
 
-        // Returns the guid in "registry" format.
-        public override string ToString() => ToString("D", null);
-
         public override int GetHashCode()
         {
             // Simply XOR all the bits of the GUID 32 bits at a time.
@@ -1054,192 +1052,221 @@ namespace System
 
         public static bool operator !=(Guid a, Guid b) => !EqualsCore(a, b);
 
-        public string ToString([StringSyntax(StringSyntaxAttribute.GuidFormat)] string? format)
-        {
-            return ToString(format, null);
-        }
-
-        private static unsafe int HexsToChars(char* guidChars, int a, int b)
+        private static unsafe int HexsToChars<TChar>(TChar* guidChars, int a, int b) where TChar : unmanaged, IUtfChar<TChar>
         {
-            guidChars[0] = HexConverter.ToCharLower(a >> 4);
-            guidChars[1] = HexConverter.ToCharLower(a);
+            guidChars[0] = TChar.CastFrom(HexConverter.ToCharLower(a >> 4));
+            guidChars[1] = TChar.CastFrom(HexConverter.ToCharLower(a));
 
-            guidChars[2] = HexConverter.ToCharLower(b >> 4);
-            guidChars[3] = HexConverter.ToCharLower(b);
+            guidChars[2] = TChar.CastFrom(HexConverter.ToCharLower(b >> 4));
+            guidChars[3] = TChar.CastFrom(HexConverter.ToCharLower(b));
 
             return 4;
         }
 
-        private static unsafe int HexsToCharsHexOutput(char* guidChars, int a, int b)
+        private static unsafe int HexsToCharsHexOutput<TChar>(TChar* guidChars, int a, int b) where TChar : unmanaged, IUtfChar<TChar>
         {
-            guidChars[0] = '0';
-            guidChars[1] = 'x';
+            guidChars[0] = TChar.CastFrom('0');
+            guidChars[1] = TChar.CastFrom('x');
 
-            guidChars[2] = HexConverter.ToCharLower(a >> 4);
-            guidChars[3] = HexConverter.ToCharLower(a);
+            guidChars[2] = TChar.CastFrom(HexConverter.ToCharLower(a >> 4));
+            guidChars[3] = TChar.CastFrom(HexConverter.ToCharLower(a));
 
-            guidChars[4] = ',';
-            guidChars[5] = '0';
-            guidChars[6] = 'x';
+            guidChars[4] = TChar.CastFrom(',');
+            guidChars[5] = TChar.CastFrom('0');
+            guidChars[6] = TChar.CastFrom('x');
 
-            guidChars[7] = HexConverter.ToCharLower(b >> 4);
-            guidChars[8] = HexConverter.ToCharLower(b);
+            guidChars[7] = TChar.CastFrom(HexConverter.ToCharLower(b >> 4));
+            guidChars[8] = TChar.CastFrom(HexConverter.ToCharLower(b));
 
             return 9;
         }
 
+        // Returns the guid in "registry" format.
+        public override string ToString() => ToString("d", null);
+
+        public string ToString([StringSyntax(StringSyntaxAttribute.GuidFormat)] string? format)
+        {
+            return ToString(format, null);
+        }
+
         // IFormattable interface
         // We currently ignore provider
         public string ToString([StringSyntax(StringSyntaxAttribute.GuidFormat)] string? format, IFormatProvider? provider)
         {
+            int guidSize;
             if (string.IsNullOrEmpty(format))
             {
-                format = "D";
+                guidSize = 36;
             }
-
-            // all acceptable format strings are of length 1
-            if (format.Length != 1)
+            else
             {
-                throw new FormatException(SR.Format_InvalidGuidFormatSpecification);
-            }
+                // all acceptable format strings are of length 1
+                if (format.Length != 1)
+                {
+                    ThrowBadGuidFormatSpecification();
+                }
 
-            int guidSize;
-            switch (format[0])
-            {
-                case 'D':
-                case 'd':
-                    guidSize = 36;
-                    break;
-                case 'N':
-                case 'n':
-                    guidSize = 32;
-                    break;
-                case 'B':
-                case 'b':
-                case 'P':
-                case 'p':
-                    guidSize = 38;
-                    break;
-                case 'X':
-                case 'x':
-                    guidSize = 68;
-                    break;
-                default:
-                    throw new FormatException(SR.Format_InvalidGuidFormatSpecification);
+                switch (format[0] | 0x20)
+                {
+                    case 'd':
+                        guidSize = 36;
+                        break;
+
+                    case 'n':
+                        guidSize = 32;
+                        break;
+
+                    case 'b' or 'p':
+                        guidSize = 38;
+                        break;
+
+                    case 'x':
+                        guidSize = 68;
+                        break;
+
+                    default:
+                        guidSize = 0;
+                        ThrowBadGuidFormatSpecification();
+                        break;
+                };
             }
 
             string guidString = string.FastAllocateString(guidSize);
 
-            bool result = TryFormat(new Span<char>(ref guidString.GetRawStringData(), guidString.Length), out int bytesWritten, format);
+            bool result = TryFormatCore(new Span<char>(ref guidString.GetRawStringData(), guidString.Length), out int bytesWritten, format);
             Debug.Assert(result && bytesWritten == guidString.Length, "Formatting guid should have succeeded.");
 
             return guidString;
         }
 
-        // Returns whether the guid is successfully formatted as a span.
-        public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.GuidFormat)] ReadOnlySpan<char> format = default)
+        public bool TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.GuidFormat)] ReadOnlySpan<char> format = default) =>
+            TryFormatCore(destination, out charsWritten, format);
+
+        bool ISpanFormattable.TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.GuidFormat)] ReadOnlySpan<char> format, IFormatProvider? provider) =>
+            // Provider is ignored.
+            TryFormatCore(destination, out charsWritten, format);
+
+        public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.GuidFormat)] ReadOnlySpan<char> format = default) =>
+            TryFormatCore(utf8Destination, out bytesWritten, format);
+
+        bool IUtf8SpanFormattable.TryFormat(Span<byte> utf8Destination, out int bytesWritten, [StringSyntax(StringSyntaxAttribute.GuidFormat)] ReadOnlySpan<char> format, IFormatProvider? provider) =>
+            // Provider is ignored.
+            TryFormatCore(utf8Destination, out bytesWritten, format);
+
+        // TryFormatCore accepts an `int flags` composed of:
+        // - Lowest byte: required length
+        // - Second byte: opening brace char, or 0 if no braces
+        // - Third byte: closing brace char, or 0 if no braces
+        // - Highest bit: 1 if use dashes, else 0
+        internal const int TryFormatFlags_UseDashes = unchecked((int)0x80000000);
+        internal const int TryFormatFlags_CurlyBraces = ('}' << 16) | ('{' << 8);
+        internal const int TryFormatFlags_Parens = (')' << 16) | ('(' << 8);
+
+        private bool TryFormatCore<TChar>(Span<TChar> destination, out int charsWritten, ReadOnlySpan<char> format) where TChar : unmanaged, IUtfChar<TChar>
         {
+            int flags;
+
             if (format.Length == 0)
             {
-                format = "D";
+                flags = 36 + TryFormatFlags_UseDashes;
             }
-            // all acceptable format strings are of length 1
-            if (format.Length != 1)
+            else
             {
-                throw new FormatException(SR.Format_InvalidGuidFormatSpecification);
-            }
+                if (format.Length != 1)
+                {
+                    ThrowBadGuidFormatSpecification();
+                }
 
-            bool dash = true;
-            bool hex = false;
-            int braces = 0;
+                switch (format[0] | 0x20)
+                {
+                    case 'd':
+                        flags = 36 + TryFormatFlags_UseDashes;
+                        break;
 
-            int guidSize;
+                    case 'p':
+                        flags = 38 + TryFormatFlags_UseDashes + TryFormatFlags_Parens;
+                        break;
 
-            switch (format[0])
-            {
-                case 'D':
-                case 'd':
-                    guidSize = 36;
-                    break;
-                case 'N':
-                case 'n':
-                    dash = false;
-                    guidSize = 32;
-                    break;
-                case 'B':
-                case 'b':
-                    braces = '{' + ('}' << 16);
-                    guidSize = 38;
-                    break;
-                case 'P':
-                case 'p':
-                    braces = '(' + (')' << 16);
-                    guidSize = 38;
-                    break;
-                case 'X':
-                case 'x':
-                    braces = '{' + ('}' << 16);
-                    dash = false;
-                    hex = true;
-                    guidSize = 68;
-                    break;
-                default:
-                    throw new FormatException(SR.Format_InvalidGuidFormatSpecification);
+                    case 'b':
+                        flags = 38 + TryFormatFlags_UseDashes + TryFormatFlags_CurlyBraces;
+                        break;
+
+                    case 'n':
+                        flags = 32;
+                        break;
+
+                    case 'x':
+                        return TryFormatX(destination, out charsWritten);
+
+                    default:
+                        flags = 0;
+                        ThrowBadGuidFormatSpecification();
+                        break;
+                }
             }
 
-            if (destination.Length < guidSize)
+            return TryFormatCore(destination, out charsWritten, flags);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] // only used from two callers
+        internal unsafe bool TryFormatCore<TChar>(Span<TChar> destination, out int charsWritten, int flags) where TChar : unmanaged, IUtfChar<TChar>
+        {
+            // The low byte of flags contains the required length.
+            if ((byte)flags > destination.Length)
             {
                 charsWritten = 0;
                 return false;
             }
 
-            unsafe
+            charsWritten = (byte)flags;
+            flags >>= 8;
+
+            fixed (TChar* guidChars = &MemoryMarshal.GetReference(destination))
             {
-                fixed (char* guidChars = &MemoryMarshal.GetReference(destination))
+                TChar* p = guidChars;
+
+                // The low byte of flags now contains the opening brace char (if any)
+                if ((byte)flags != 0)
                 {
-                    char* p = guidChars;
+                    *p++ = TChar.CastFrom((byte)flags);
+                }
+                flags >>= 8;
 
-                    if (braces != 0)
-                        *p++ = (char)braces;
+                if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian)
+                {
+                    // Vectorized implementation for D, N, P and B formats:
+                    // [{|(]dddddddd[-]dddd[-]dddd[-]dddd[-]dddddddddddd[}|)]
+                    (Vector128<byte> vecX, Vector128<byte> vecY, Vector128<byte> vecZ) = FormatGuidVector128Utf8(this, flags < 0 /* dash */);
 
-                    if (hex)
+                    if (typeof(TChar) == typeof(byte))
                     {
-                        // {0xdddddddd,0xdddd,0xdddd,{0xdd,0xdd,0xdd,0xdd,0xdd,0xdd,0xdd,0xdd}}
-                        *p++ = '0';
-                        *p++ = 'x';
-                        p += HexsToChars(p, _a >> 24, _a >> 16);
-                        p += HexsToChars(p, _a >> 8, _a);
-                        *p++ = ',';
-                        *p++ = '0';
-                        *p++ = 'x';
-                        p += HexsToChars(p, _b >> 8, _b);
-                        *p++ = ',';
-                        *p++ = '0';
-                        *p++ = 'x';
-                        p += HexsToChars(p, _c >> 8, _c);
-                        *p++ = ',';
-                        *p++ = '{';
-                        p += HexsToCharsHexOutput(p, _d, _e);
-                        *p++ = ',';
-                        p += HexsToCharsHexOutput(p, _f, _g);
-                        *p++ = ',';
-                        p += HexsToCharsHexOutput(p, _h, _i);
-                        *p++ = ',';
-                        p += HexsToCharsHexOutput(p, _j, _k);
-                        *p++ = '}';
+                        byte* pChar = (byte*)p;
+                        if (flags < 0 /* dash */)
+                        {
+                            // We need to merge these vectors in this order:
+                            // xxxxxxxxxxxxxxxx
+                            //                     yyyyyyyyyyyyyyyy
+                            //         zzzzzzzzzzzzzzzz
+                            vecX.Store(pChar);
+                            vecY.Store(pChar + 20);
+                            vecZ.Store(pChar + 8);
+                            p += 36;
+                        }
+                        else
+                        {
+                            // xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyy
+                            vecX.Store(pChar);
+                            vecY.Store(pChar + 16);
+                            p += 32;
+                        }
                     }
-                    else if ((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian)
+                    else
                     {
-                        // Vectorized implementation for D, N, P and B formats:
-                        // [{|(]dddddddd[-]dddd[-]dddd[-]dddd[-]dddddddddddd[}|)]
-                        (Vector128<byte> vecX, Vector128<byte> vecY, Vector128<byte> vecZ) =
-                            Buffers.Text.Utf8Formatter.FormatGuidVector128Utf8(this, dash);
-
                         // Expand to UTF-16
                         (Vector128<ushort> x0, Vector128<ushort> x1) = Vector128.Widen(vecX);
                         (Vector128<ushort> y0, Vector128<ushort> y1) = Vector128.Widen(vecY);
                         ushort* pChar = (ushort*)p;
-                        if (dash)
+                        if (flags < 0 /* dash */)
                         {
                             (Vector128<ushort> z0, Vector128<ushort> z1) = Vector128.Widen(vecZ);
 
@@ -1247,7 +1274,7 @@ namespace System
                             // xxxxxxxxxxxxxxxx
                             //                     yyyyyyyyyyyyyyyy
                             //         zzzzzzzzzzzzzzzz
-                            x0.Store(pChar + 0);
+                            x0.Store(pChar);
                             y0.Store(pChar + 20);
                             y1.Store(pChar + 28);
                             z0.Store(pChar + 8); // overlaps x1
@@ -1257,54 +1284,153 @@ namespace System
                         else
                         {
                             // xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyy
-                            x0.Store(pChar + 0);
+                            x0.Store(pChar);
                             x1.Store(pChar + 8);
                             y0.Store(pChar + 16);
                             y1.Store(pChar + 24);
                             p += 32;
                         }
-                        if (braces != 0)
-                            *p = (char)(braces >> 16);
-                        charsWritten = guidSize;
-                        return true;
                     }
-                    else
+                }
+                else
+                {
+                    // Non-vectorized fallback for D, N, P and B formats:
+                    // [{|(]dddddddd[-]dddd[-]dddd[-]dddd[-]dddddddddddd[}|)]
+                    p += HexsToChars(p, _a >> 24, _a >> 16);
+                    p += HexsToChars(p, _a >> 8, _a);
+                    if (flags < 0 /* dash */)
                     {
-                        // Non-vectorized fallback for D, N, P and B formats:
-                        // [{|(]dddddddd[-]dddd[-]dddd[-]dddd[-]dddddddddddd[}|)]
-                        p += HexsToChars(p, _a >> 24, _a >> 16);
-                        p += HexsToChars(p, _a >> 8, _a);
-                        if (dash)
-                            *p++ = '-';
-                        p += HexsToChars(p, _b >> 8, _b);
-                        if (dash)
-                            *p++ = '-';
-                        p += HexsToChars(p, _c >> 8, _c);
-                        if (dash)
-                            *p++ = '-';
-                        p += HexsToChars(p, _d, _e);
-                        if (dash)
-                            *p++ = '-';
-                        p += HexsToChars(p, _f, _g);
-                        p += HexsToChars(p, _h, _i);
-                        p += HexsToChars(p, _j, _k);
+                        *p++ = TChar.CastFrom('-');
                     }
+                    p += HexsToChars(p, _b >> 8, _b);
+                    if (flags < 0 /* dash */)
+                    {
+                        *p++ = TChar.CastFrom('-');
+                    }
+                    p += HexsToChars(p, _c >> 8, _c);
+                    if (flags < 0 /* dash */)
+                    {
+                        *p++ = TChar.CastFrom('-');
+                    }
+                    p += HexsToChars(p, _d, _e);
+                    if (flags < 0 /* dash */)
+                    {
+                        *p++ = TChar.CastFrom('-');
+                    }
+                    p += HexsToChars(p, _f, _g);
+                    p += HexsToChars(p, _h, _i);
+                    p += HexsToChars(p, _j, _k);
+                }
 
-                    if (braces != 0)
-                        *p++ = (char)(braces >> 16);
-
-                    Debug.Assert(p - guidChars == guidSize);
+                // The low byte of flags now contains the closing brace char (if any)
+                if ((byte)flags != 0)
+                {
+                    *p = TChar.CastFrom((byte)flags);
                 }
+
+                Debug.Assert(p == guidChars + charsWritten - ((byte)flags != 0 ? 1 : 0));
+            }
+
+            return true;
+        }
+
+        private unsafe bool TryFormatX<TChar>(Span<TChar> destination, out int charsWritten) where TChar : unmanaged, IUtfChar<TChar>
+        {
+            if (destination.Length < 68)
+            {
+                charsWritten = 0;
+                return false;
+            }
+            charsWritten = 68;
+
+            fixed (TChar* guidChars = &MemoryMarshal.GetReference(destination))
+            {
+                TChar* p = guidChars;
+
+                // {0xdddddddd,0xdddd,0xdddd,{0xdd,0xdd,0xdd,0xdd,0xdd,0xdd,0xdd,0xdd}}
+                *p++ = TChar.CastFrom('{');
+                *p++ = TChar.CastFrom('0');
+                *p++ = TChar.CastFrom('x');
+                p += HexsToChars(p, _a >> 24, _a >> 16);
+                p += HexsToChars(p, _a >> 8, _a);
+                *p++ = TChar.CastFrom(',');
+                *p++ = TChar.CastFrom('0');
+                *p++ = TChar.CastFrom('x');
+                p += HexsToChars(p, _b >> 8, _b);
+                *p++ = TChar.CastFrom(',');
+                *p++ = TChar.CastFrom('0');
+                *p++ = TChar.CastFrom('x');
+                p += HexsToChars(p, _c >> 8, _c);
+                *p++ = TChar.CastFrom(',');
+                *p++ = TChar.CastFrom('{');
+                p += HexsToCharsHexOutput(p, _d, _e);
+                *p++ = TChar.CastFrom(',');
+                p += HexsToCharsHexOutput(p, _f, _g);
+                *p++ = TChar.CastFrom(',');
+                p += HexsToCharsHexOutput(p, _h, _i);
+                *p++ = TChar.CastFrom(',');
+                p += HexsToCharsHexOutput(p, _j, _k);
+                *p++ = TChar.CastFrom('}');
+                *p = TChar.CastFrom('}');
+
+                Debug.Assert(p == guidChars + charsWritten - 1);
             }
 
-            charsWritten = guidSize;
             return true;
         }
 
-        bool ISpanFormattable.TryFormat(Span<char> destination, out int charsWritten, [StringSyntax(StringSyntaxAttribute.GuidFormat)] ReadOnlySpan<char> format, IFormatProvider? provider)
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static (Vector128<byte>, Vector128<byte>, Vector128<byte>) FormatGuidVector128Utf8(Guid value, bool useDashes)
         {
-            // Like with the IFormattable implementation, provider is ignored.
-            return TryFormat(destination, out charsWritten, format);
+            Debug.Assert((Ssse3.IsSupported || AdvSimd.Arm64.IsSupported) && BitConverter.IsLittleEndian);
+            // Vectorized implementation for D, N, P and B formats:
+            // [{|(]dddddddd[-]dddd[-]dddd[-]dddd[-]dddddddddddd[}|)]
+
+            Vector128<byte> hexMap = Vector128.Create(
+                (byte)'0', (byte)'1', (byte)'2', (byte)'3',
+                (byte)'4', (byte)'5', (byte)'6', (byte)'7',
+                (byte)'8', (byte)'9', (byte)'a', (byte)'b',
+                (byte)'c', (byte)'d', (byte)'e', (byte)'f');
+
+            Vector128<byte> srcVec = Unsafe.As<Guid, Vector128<byte>>(ref value);
+            (Vector128<byte> hexLow, Vector128<byte> hexHigh) =
+                HexConverter.AsciiToHexVector128(srcVec, hexMap);
+
+            // because of Guid's layout (int _a, short _b, _c, <8 byte fields>)
+            // we have to shuffle some bytes for _a, _b and _c
+            hexLow = Vector128.Shuffle(hexLow.AsInt16(), Vector128.Create(3, 2, 1, 0, 5, 4, 7, 6)).AsByte();
+
+            if (useDashes)
+            {
+                // We divide 16 bytes into 3 x Vector128<byte>:
+                //
+                // ________-____-____-____-____________
+                // xxxxxxxxxxxxxxxx
+                //                     yyyyyyyyyyyyyyyy
+                //         zzzzzzzzzzzzzzzz
+                //
+                // Vector "x" - just one dash, shift all elements after it.
+                Vector128<byte> vecX = Vector128.Shuffle(hexLow,
+                    Vector128.Create(0x706050403020100, 0xD0CFF0B0A0908FF).AsByte());
+
+                // Vector "y" - same here.
+                Vector128<byte> vecY = Vector128.Shuffle(hexHigh,
+                    Vector128.Create(0x7060504FF030201, 0xF0E0D0C0B0A0908).AsByte());
+
+                // Vector "z" - we need to merge some elements of hexLow with hexHigh and add 4 dashes.
+                Vector128<byte> mid1 = Vector128.Shuffle(hexLow,
+                    Vector128.Create(0x0D0CFF0B0A0908FF, 0xFFFFFFFFFFFF0F0E).AsByte());
+                Vector128<byte> mid2 = Vector128.Shuffle(hexHigh,
+                    Vector128.Create(0xFFFFFFFFFFFFFFFF, 0xFF03020100FFFFFF).AsByte());
+                Vector128<byte> dashesMask = Vector128.Shuffle(Vector128.CreateScalarUnsafe((byte)'-'),
+                    Vector128.Create(0xFFFF00FFFFFFFF00, 0x00FFFFFFFF00FFFF).AsByte());
+
+                Vector128<byte> vecZ = (mid1 | mid2 | dashesMask);
+                return (vecX, vecY, vecZ);
+            }
+
+            // N format - no dashes.
+            return (hexLow, hexHigh, default);
         }
 
         //
@@ -1574,5 +1700,9 @@ namespace System
 
         /// <inheritdoc cref="ISpanParsable{TSelf}.TryParse(ReadOnlySpan{char}, IFormatProvider?, out TSelf)" />
         public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out Guid result) => TryParse(s, out result);
+
+        [DoesNotReturn]
+        private static void ThrowBadGuidFormatSpecification() =>
+            throw new FormatException(SR.Format_InvalidGuidFormatSpecification);
     }
 }
index a9fd59e5af115527aa180923bd47d0dda588f86d..251edf1a3687bd8b81070d324a0f45c3ffbc4865 100644 (file)
@@ -2758,7 +2758,7 @@ namespace System
         Timeout = 3,
         NotApplicable = 4,
     }
-    public readonly partial struct Guid : System.IComparable, System.IComparable<System.Guid>, System.IEquatable<System.Guid>, System.IFormattable, System.IParsable<System.Guid>, System.ISpanFormattable, System.ISpanParsable<System.Guid>
+    public readonly partial struct Guid : System.IComparable, System.IComparable<System.Guid>, System.IEquatable<System.Guid>, System.IFormattable, System.IParsable<System.Guid>, System.ISpanFormattable, System.ISpanParsable<System.Guid>, System.IUtf8SpanFormattable
     {
         private readonly int _dummyPrimitive;
         public static readonly System.Guid Empty;
@@ -2788,11 +2788,13 @@ namespace System
         public static System.Guid ParseExact(System.ReadOnlySpan<char> input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] System.ReadOnlySpan<char> format) { throw null; }
         public static System.Guid ParseExact(string input, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] string format) { throw null; }
         bool System.ISpanFormattable.TryFormat(System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
+        bool System.IUtf8SpanFormattable.TryFormat(System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
         public byte[] ToByteArray() { throw null; }
         public override string ToString() { throw null; }
         public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] string? format) { throw null; }
         public string ToString([System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] string? format, System.IFormatProvider? provider) { throw null; }
         public bool TryFormat(System.Span<char> destination, out int charsWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] System.ReadOnlySpan<char> format = default(System.ReadOnlySpan<char>)) { throw null; }
+        public bool TryFormat(System.Span<byte> utf8Destination, out int bytesWritten, [System.Diagnostics.CodeAnalysis.StringSyntaxAttribute("GuidFormat")] System.ReadOnlySpan<char> format = default(System.ReadOnlySpan<char>)) { throw null; }
         public static bool TryParse(System.ReadOnlySpan<char> input, out System.Guid result) { throw null; }
         public static bool TryParse(System.ReadOnlySpan<char> s, System.IFormatProvider? provider, out System.Guid result) { throw null; }
         public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, out System.Guid result) { throw null; }
index 12742ad5df1f12766bbc389c2de23b1f1ad7715b..f71efb5d0e8df933c738687c70c55a7af0e8a3e5 100644 (file)
@@ -4,6 +4,7 @@
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using System.Text;
 using Xunit;
 
 namespace System.Tests
@@ -847,6 +848,12 @@ namespace System.Tests
         {
             Assert.Throws<FormatException>(() => s_testGuid.TryFormat(new Span<char>(), out int charsWritten, format));
             Assert.Throws<FormatException>(() => s_testGuid.TryFormat(new Span<char>(), out int charsWritten, format.ToUpperInvariant()));
+
+            Assert.Throws<FormatException>(() => ((ISpanFormattable)s_testGuid).TryFormat(new Span<char>(), out int charsWritten, format, null));
+            Assert.Throws<FormatException>(() => ((ISpanFormattable)s_testGuid).TryFormat(new Span<char>(), out int charsWritten, format.ToUpperInvariant(), null));
+
+            Assert.Throws<FormatException>(() => s_testGuid.TryFormat(new Span<byte>(), out int bytesWritten, format));
+            Assert.Throws<FormatException>(() => s_testGuid.TryFormat(new Span<byte>(), out int bytesWritten, format.ToUpperInvariant()));
         }
 
         [Theory]
@@ -854,8 +861,18 @@ namespace System.Tests
         public static void TryFormat_LengthTooSmall_ReturnsFalse(Guid guid, string format, string expected)
         {
             _ = expected;
+
             Assert.False(guid.TryFormat(new Span<char>(new char[guid.ToString(format).Length - 1]), out int charsWritten, format));
             Assert.Equal(0, charsWritten);
+
+            Assert.False(((ISpanFormattable)guid).TryFormat(new Span<char>(new char[guid.ToString(format).Length - 1]), out charsWritten, format, null));
+            Assert.Equal(0, charsWritten);
+
+            Assert.False(guid.TryFormat(new Span<byte>(new byte[guid.ToString(format).Length - 1]), out int bytesWritten, format));
+            Assert.Equal(0, bytesWritten);
+
+            Assert.False(((IUtf8SpanFormattable)guid).TryFormat(new Span<byte>(new byte[guid.ToString(format).Length - 1]), out bytesWritten, format, null));
+            Assert.Equal(0, bytesWritten);
         }
 
         [Theory]
@@ -863,17 +880,45 @@ namespace System.Tests
         public static void TryFormat_CharsWritten_EqualsZero_WhenSpanTooSmall(Guid guid, string format, string expected)
         {
             _ = expected;
+
             Assert.False(guid.TryFormat(new Span<char>(new char[guid.ToString(format).Length - 1]), out int charsWritten, format));
             Assert.Equal(0, charsWritten);
+
+            Assert.False(((ISpanFormattable)guid).TryFormat(new Span<char>(new char[guid.ToString(format).Length - 1]), out charsWritten, format, null));
+            Assert.Equal(0, charsWritten);
+
+            Assert.False(guid.TryFormat(new Span<byte>(new byte[guid.ToString(format).Length - 1]), out int bytesWritten, format));
+            Assert.Equal(0, bytesWritten);
+
+            Assert.False(((IUtf8SpanFormattable)guid).TryFormat(new Span<byte>(new byte[guid.ToString(format).Length - 1]), out bytesWritten, format, null));
+            Assert.Equal(0, bytesWritten);
         }
 
         [Theory]
         [MemberData(nameof(ToString_TestData))]
         public static void TryFormat_ValidLength_ReturnsTrue(Guid guid, string format, string expected)
         {
-            char[] chars = new char[guid.ToString(format).Length];
-            Assert.True(guid.TryFormat(new Span<char>(chars), out int charsWritten, format));
-            Assert.Equal(chars, expected.ToCharArray());
+            for (int additionalSpace = 0; additionalSpace < 2; additionalSpace++)
+            {
+                char[] chars = new char[expected.Length + additionalSpace];
+                byte[] bytes = new byte[expected.Length + additionalSpace];
+
+                Assert.True(guid.TryFormat(new Span<char>(chars), out int charsWritten, format));
+                Assert.Equal(expected.Length, charsWritten);
+                Assert.Equal(expected, chars.AsSpan(0, charsWritten).ToString());
+
+                Assert.True(((ISpanFormattable)guid).TryFormat(new Span<char>(chars), out charsWritten, format, null));
+                Assert.Equal(expected.Length, charsWritten);
+                Assert.Equal(expected, chars.AsSpan(0, charsWritten).ToString());
+
+                Assert.True(guid.TryFormat(new Span<byte>(bytes), out int bytesWritten, format));
+                Assert.Equal(expected.Length, bytesWritten);
+                Assert.Equal(expected, Encoding.UTF8.GetString(bytes.AsSpan(0, bytesWritten)));
+
+                Assert.True(((IUtf8SpanFormattable)guid).TryFormat(new Span<byte>(bytes), out bytesWritten, format, null));
+                Assert.Equal(expected.Length, bytesWritten);
+                Assert.Equal(expected, Encoding.UTF8.GetString(bytes.AsSpan(0, bytesWritten)));
+            }
         }
     }
 }