Update interpolated string handler based on API reviews / LDM decisions (#53153)
authorStephen Toub <stoub@microsoft.com>
Tue, 25 May 2021 21:50:49 +0000 (17:50 -0400)
committerGitHub <noreply@github.com>
Tue, 25 May 2021 21:50:49 +0000 (17:50 -0400)
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs [moved from src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringBuilder.cs with 82% similarity]
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringHandlerArgumentAttribute.cs [new file with mode: 0644]
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringHandlerAttribute.cs [new file with mode: 0644]
src/libraries/System.Runtime/ref/System.Runtime.cs
src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj
src/libraries/System.Runtime/tests/System/Runtime/CompilerServices/AttributesTests.cs
src/libraries/System.Runtime/tests/System/Runtime/CompilerServices/DefaultInterpolatedStringHandlerTests.cs [moved from src/libraries/System.Runtime/tests/System/Runtime/CompilerServices/InterpolatedStringBuilderTests.cs with 86% similarity]

index 7e42770..fb5011f 100644 (file)
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\IsConst.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\IsReadOnlyAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\IsVolatile.cs" />
-    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\InterpolatedStringBuilder.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\InterpolatedStringHandlerAttribute.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\InterpolatedStringHandlerArgumentAttribute.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\DefaultInterpolatedStringHandler.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\IteratorStateMachineAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\ITuple.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\LoadHint.cs" />
@@ -7,20 +7,21 @@ using System.Globalization;
 
 namespace System.Runtime.CompilerServices
 {
-    /// <summary>Provides a builder used by the language compiler to process interpolated strings into <see cref="string"/> instances.</summary>
-    public ref struct InterpolatedStringBuilder
+    /// <summary>Provides a handler used by the language compiler to process interpolated strings into <see cref="string"/> instances.</summary>
+    [InterpolatedStringHandler]
+    public ref struct DefaultInterpolatedStringHandler
     {
         // Implementation note:
         // As this type lives in CompilerServices and is only intended to be targeted by the compiler,
         // public APIs eschew argument validation logic in a variety of places, e.g. allowing a null input
         // when one isn't expected to produce a NullReferenceException rather than an ArgumentNullException.
 
-        /// <summary>Expected average length of formatted data used for an individual hole.</summary>
+        /// <summary>Expected average length of formatted data used for an individual interpolation expression result.</summary>
         /// <remarks>
         /// This is inherited from string.Format, and could be changed based on further data.
         /// string.Format actually uses `format.Length + args.Length * 8`, but format.Length
-        /// includes the holes themselves, e.g. "{0}", and since it's rare to have double-digit
-        /// numbers of holes, we bump the 8 up to 11 to account for the three extra characters in "{d}",
+        /// includes the format items themselves, e.g. "{0}", and since it's rare to have double-digit
+        /// numbers of items, we bump the 8 up to 11 to account for the three extra characters in "{d}",
         /// since the compiler-provided base length won't include the equivalent character count.
         /// </remarks>
         private const int GuessedLengthPerHole = 11;
@@ -40,122 +41,94 @@ namespace System.Runtime.CompilerServices
         /// <remarks>
         /// Custom formatters are very rare.  We want to support them, but it's ok if we make them more expensive
         /// in order to make them as pay-for-play as possible.  So, we avoid adding another reference type field
-        /// to reduce the size of the builder and to reduce required zero'ing, by only storing whether the provider
+        /// to reduce the size of the handler and to reduce required zero'ing, by only storing whether the provider
         /// provides a formatter, rather than actually storing the formatter.  This in turn means, if there is a
         /// formatter, we pay for the extra interface call on each AppendFormatted that needs it.
         /// </remarks>
         private readonly bool _hasCustomFormatter;
 
-        /// <summary>Initializes the builder.</summary>
-        /// <param name="initialCapacity">Approximated capacity required to support the interpolated string.  The final size may be smaller or larger.</param>
-        private InterpolatedStringBuilder(int initialCapacity)
+        /// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
+        /// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
+        /// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
+        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
+        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount)
         {
             _provider = null;
-            _chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
+            _chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
             _pos = 0;
             _hasCustomFormatter = false;
         }
 
-        /// <summary>Initializes the builder.</summary>
-        /// <param name="scratchBuffer">A buffer temporarily transferred to the builder for use as part of its formatting.  Contents may be overwritten.</param>
-        private InterpolatedStringBuilder(Span<char> scratchBuffer)
-        {
-            _provider = null;
-            _arrayToReturnToPool = null;
-            _chars = scratchBuffer;
-            _pos = 0;
-            _hasCustomFormatter = false;
-        }
-
-        /// <summary>Initializes the builder.</summary>
-        /// <param name="initialCapacity">Approximated capacity required to support the interpolated string.  The final size may be smaller or larger.</param>
+        /// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
+        /// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
+        /// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
         /// <param name="provider">An object that supplies culture-specific formatting information.</param>
-        private InterpolatedStringBuilder(int initialCapacity, IFormatProvider? provider)
+        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
+        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider)
         {
             _provider = provider;
-            _chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
+            _chars = _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
             _pos = 0;
             _hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
         }
 
-        /// <summary>Initializes the builder.</summary>
-        /// <param name="scratchBuffer">A buffer temporarily transferred to the builder for use as part of its formatting.  Contents may be overwritten.</param>
+        /// <summary>Creates a handler used to translate an interpolated string into a <see cref="string"/>.</summary>
+        /// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
+        /// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
         /// <param name="provider">An object that supplies culture-specific formatting information.</param>
-        private InterpolatedStringBuilder(Span<char> scratchBuffer, IFormatProvider? provider)
+        /// <param name="initialBuffer">A buffer temporarily transferred to the handler for use as part of its formatting.  Contents may be overwritten.</param>
+        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
+        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Span<char> initialBuffer)
         {
             _provider = provider;
+            _chars = initialBuffer;
             _arrayToReturnToPool = null;
-            _chars = scratchBuffer;
             _pos = 0;
             _hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
         }
 
-        /// <summary>Creates a builder used to translate an interpolated string into a <see cref="string"/>.</summary>
-        /// <param name="literalLength">The number of constant characters outside of holes in the interpolated string.</param>
-        /// <param name="formattedCount">The number of holes in the interpolated string.</param>
-        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
-        public static InterpolatedStringBuilder Create(int literalLength, int formattedCount) =>
-            new InterpolatedStringBuilder(GetDefaultLength(literalLength, formattedCount));
-
-        /// <summary>Creates a builder used to translate an interpolated string into a <see cref="string"/>.</summary>
-        /// <param name="literalLength">The number of constant characters outside of holes in the interpolated string.</param>
-        /// <param name="formattedCount">The number of holes in the interpolated string.</param>
-        /// <param name="provider">An object that supplies culture-specific formatting information.</param>
-        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
-        public static InterpolatedStringBuilder Create(int literalLength, int formattedCount, IFormatProvider? provider) =>
-            new InterpolatedStringBuilder(GetDefaultLength(literalLength, formattedCount), provider);
-
-        /// <summary>Creates a builder used to translate an interpolated string into a <see cref="string"/>.</summary>
-        /// <param name="literalLength">The number of constant characters outside of holes in the interpolated string.</param>
-        /// <param name="formattedCount">The number of holes in the interpolated string.</param>
-        /// <param name="scratchBuffer">A buffer temporarily transferred to the builder for use as part of its formatting.  Contents may be overwritten.</param>
-        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
-        public static InterpolatedStringBuilder Create(int literalLength, int formattedCount, Span<char> scratchBuffer) =>
-            new InterpolatedStringBuilder(scratchBuffer);
-
-        /// <summary>Creates a builder used to translate an interpolated string into a <see cref="string"/>.</summary>
-        /// <param name="literalLength">The number of constant characters outside of holes in the interpolated string.</param>
-        /// <param name="formattedCount">The number of holes in the interpolated string.</param>
-        /// <param name="provider">An object that supplies culture-specific formatting information.</param>
-        /// <param name="scratchBuffer">A buffer temporarily transferred to the builder for use as part of its formatting.  Contents may be overwritten.</param>
-        /// <remarks>This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.</remarks>
-        public static InterpolatedStringBuilder Create(int literalLength, int formattedCount, IFormatProvider? provider, Span<char> scratchBuffer) =>
-            new InterpolatedStringBuilder(scratchBuffer, provider);
-
-        /// <summary>Derives a default length with which to seed the builder.</summary>
-        /// <param name="literalLength">The number of constant characters outside of holes in the interpolated string.</param>
-        /// <param name="formattedCount">The number of holes in the interpolated string.</param>
+        /// <summary>Derives a default length with which to seed the handler.</summary>
+        /// <param name="literalLength">The number of constant characters outside of interpolation expressions in the interpolated string.</param>
+        /// <param name="formattedCount">The number of interpolation expressions in the interpolated string.</param>
         [MethodImpl(MethodImplOptions.AggressiveInlining)] // becomes a constant when inputs are constant
         private static int GetDefaultLength(int literalLength, int formattedCount) =>
             Math.Max(MinimumArrayPoolLength, literalLength + (formattedCount * GuessedLengthPerHole));
 
         /// <summary>Gets the built <see cref="string"/>.</summary>
         /// <returns>The built string.</returns>
-        public override string ToString() => new string(_chars.Slice(0, _pos));
+        public override string ToString() => new string(Text);
 
-        /// <summary>Gets the built <see cref="string"/> and clears the builder.</summary>
+        /// <summary>Gets the built <see cref="string"/> and clears the handler.</summary>
         /// <returns>The built string.</returns>
         /// <remarks>
-        /// This releases any resources used by the builder. The method should be invoked only
-        /// once and as the last thing performed on the builder. Subsequent use is erroneous, ill-defined,
-        /// and may destabilize the process, as may using any other copies of the builder after ToStringAndClear
+        /// This releases any resources used by the handler. The method should be invoked only
+        /// once and as the last thing performed on the handler. Subsequent use is erroneous, ill-defined,
+        /// and may destabilize the process, as may using any other copies of the handler after ToStringAndClear
         /// is called on any one of them.
         /// </remarks>
         public string ToStringAndClear()
         {
-            string result = new string(_chars.Slice(0, _pos));
+            string result = new string(Text);
+            Clear();
+            return result;
+        }
 
+        /// <summary>Clears the handler, returning any rented array to the pool.</summary>
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] // used only on a few hot paths
+        internal void Clear()
+        {
             char[]? toReturn = _arrayToReturnToPool;
             this = default; // defensive clear
             if (toReturn is not null)
             {
                 ArrayPool<char>.Shared.Return(toReturn);
             }
-
-            return result;
         }
 
-        /// <summary>Writes the specified string to the builder.</summary>
+        /// <summary>Gets a span of the written characters thus far.</summary>
+        internal ReadOnlySpan<char> Text => _chars.Slice(0, _pos);
+
+        /// <summary>Writes the specified string to the handler.</summary>
         /// <param name="value">The string to write.</param>
         public void AppendLiteral(string value)
         {
@@ -171,9 +144,9 @@ namespace System.Runtime.CompilerServices
 
         #region AppendFormatted
         // Design note:
-        // The compiler requires a AppendFormatted overload for anything that might be within a hole;
-        // if it can't find an appropriate overload, for builders in general it'll simply fail to compile.
-        // (For target-typing to string where it uses InterpolatedStringBuilder implicitly, it'll instead fall back to
+        // The compiler requires a AppendFormatted overload for anything that might be within an interpolation expression;
+        // if it can't find an appropriate overload, for handlers in general it'll simply fail to compile.
+        // (For target-typing to string where it uses DefaultInterpolatedStringHandler implicitly, it'll instead fall back to
         // its other mechanisms, e.g. using string.Format.  This fallback has the benefit that if we miss a case,
         // interpolated strings will still work, but it has the downside that a developer generally won't know
         // if the fallback is happening and they're paying more.)
@@ -182,8 +155,8 @@ namespace System.Runtime.CompilerServices
         //     (object value, int alignment = 0, string? format = null)
         // Such an overload would provide the same expressiveness as string.Format.  However, this has several
         // shortcomings:
-        // - Every value type in a hole would be boxed.
-        // - ReadOnlySpan<char> could not be used in holes.
+        // - Every value type in an interpolation expression would be boxed.
+        // - ReadOnlySpan<char> could not be used in interpolation expressions.
         // - Every AppendFormatted call would have three arguments at the call site, bloating the IL further.
         // - Every invocation would be more expensive, due to lack of specialization, every call needing to account
         //   for alignment and format, etc.
@@ -199,7 +172,7 @@ namespace System.Runtime.CompilerServices
         //     (ReadOnlySpan<char>, int alignment, string? format)
         // but this also has shortcomings:
         // - Some expressions that would have worked with an object overload will now force a fallback to string.Format
-        //   (or fail to compile if the builder is used in places where the fallback isn't provided), because the compiler
+        //   (or fail to compile if the handler is used in places where the fallback isn't provided), because the compiler
         //   can't always target type to T, e.g. `b switch { true => 1, false => null }` where `b` is a bool can successfully
         //   be passed as an argument of type `object` but not of type `T`.
         // - Reference types get no benefit from going through the generic code paths, and actually incur some overheads
@@ -208,7 +181,7 @@ namespace System.Runtime.CompilerServices
         //   at compile time for value types but don't (currently) if the Nullable<T> goes through the same code paths
         //   (see https://github.com/dotnet/runtime/issues/50915).
         //
-        // We could try to take a more elaborate approach for InterpolatedStringBuilder, since it is the most common builder
+        // We could try to take a more elaborate approach for DefaultInterpolatedStringHandler, since it is the most common handler
         // and we want to minimize overheads both at runtime and in IL size, e.g. have a complete set of overloads for each of:
         //     (T, ...) where T : struct
         //     (T?, ...) where T : struct
@@ -222,7 +195,7 @@ namespace System.Runtime.CompilerServices
         // - Any reference type with an implicit cast to ROS<char> will fail to compile due to ambiguities between the overloads. string
         //   is one such type, hence needing dedicated overloads for it that can be bound to more tightly.
         //
-        // A middle ground we've settled on, which is likely to be the right approach for most other builders as well, would be the set:
+        // A middle ground we've settled on, which is likely to be the right approach for most other handlers as well, would be the set:
         //     (T, ...) with no constraint
         //     (ReadOnlySpan<char>) and (ReadOnlySpan<char>, int)
         //     (object, int alignment = 0, string? format = null)
@@ -245,7 +218,7 @@ namespace System.Runtime.CompilerServices
         // importantly which can't be boxed to be passed to ICustomFormatter.Format.
 
         #region AppendFormatted T
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         public void AppendFormatted<T>(T value)
         {
@@ -295,7 +268,7 @@ namespace System.Runtime.CompilerServices
                 AppendLiteral(s);
             }
         }
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         /// <param name="format">The format string.</param>
         public void AppendFormatted<T>(T value, string? format)
@@ -343,7 +316,7 @@ namespace System.Runtime.CompilerServices
             }
         }
 
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         /// <param name="alignment">Minimum number of characters that should be written for this value.  If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
         public void AppendFormatted<T>(T value, int alignment)
@@ -356,7 +329,7 @@ namespace System.Runtime.CompilerServices
             }
         }
 
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         /// <param name="format">The format string.</param>
         /// <param name="alignment">Minimum number of characters that should be written for this value.  If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
@@ -372,7 +345,7 @@ namespace System.Runtime.CompilerServices
         #endregion
 
         #region AppendFormatted ReadOnlySpan<char>
-        /// <summary>Writes the specified character span to the builder.</summary>
+        /// <summary>Writes the specified character span to the handler.</summary>
         /// <param name="value">The span to write.</param>
         public void AppendFormatted(ReadOnlySpan<char> value)
         {
@@ -387,7 +360,7 @@ namespace System.Runtime.CompilerServices
             }
         }
 
-        /// <summary>Writes the specified string of chars to the builder.</summary>
+        /// <summary>Writes the specified string of chars to the handler.</summary>
         /// <param name="value">The span to write.</param>
         /// <param name="alignment">Minimum number of characters that should be written for this value.  If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
         /// <param name="format">The format string.</param>
@@ -429,7 +402,7 @@ namespace System.Runtime.CompilerServices
         #endregion
 
         #region AppendFormatted string
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         public void AppendFormatted(string? value)
         {
@@ -446,7 +419,7 @@ namespace System.Runtime.CompilerServices
             }
         }
 
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         /// <remarks>
         /// Slow path to handle a custom formatter, potentially null value,
@@ -467,7 +440,7 @@ namespace System.Runtime.CompilerServices
             }
         }
 
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         /// <param name="alignment">Minimum number of characters that should be written for this value.  If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
         /// <param name="format">The format string.</param>
@@ -479,7 +452,7 @@ namespace System.Runtime.CompilerServices
         #endregion
 
         #region AppendFormatted object
-        /// <summary>Writes the specified value to the builder.</summary>
+        /// <summary>Writes the specified value to the handler.</summary>
         /// <param name="value">The value to write.</param>
         /// <param name="alignment">Minimum number of characters that should be written for this value.  If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
         /// <param name="format">The format string.</param>
@@ -492,8 +465,8 @@ namespace System.Runtime.CompilerServices
         #endregion
 
         /// <summary>Gets whether the provider provides a custom formatter.</summary>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)] // only used in two hot path call sites
-        private static bool HasCustomFormatter(IFormatProvider provider)
+        [MethodImpl(MethodImplOptions.AggressiveInlining)] // only used in a few hot path call sites
+        internal static bool HasCustomFormatter(IFormatProvider provider)
         {
             Debug.Assert(provider is not null);
             Debug.Assert(provider is not CultureInfo || provider.GetFormat(typeof(ICustomFormatter)) is null, "Expected CultureInfo to not provide a custom formatter");
@@ -524,7 +497,7 @@ namespace System.Runtime.CompilerServices
             }
         }
 
-        /// <summary>Handles adding any padding required for aligning a formatted value in a hole.</summary>
+        /// <summary>Handles adding any padding required for aligning a formatted value in an interpolation expression.</summary>
         /// <param name="startingPos">The position at which the written value started.</param>
         /// <param name="alignment">Non-zero minimum number of characters that should be written for this value.  If the value is negative, it indicates left-aligned and the required minimum is the absolute value.</param>
         private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringHandlerArgumentAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringHandlerArgumentAttribute.cs
new file mode 100644 (file)
index 0000000..1d55ae4
--- /dev/null
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Runtime.CompilerServices
+{
+    /// <summary>Indicates which arguments to a method involving an interpolated string handler should be passed to that handler.</summary>
+    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
+    public sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
+    {
+        /// <summary>Initializes a new instance of the <see cref="InterpolatedStringHandlerArgumentAttribute"/> class.</summary>
+        /// <param name="argument">The name of the argument that should be passed to the handler.</param>
+        /// <remarks><see langword="null"/> may be used as the name of the receiver in an instance method.</remarks>
+        public InterpolatedStringHandlerArgumentAttribute(string argument) => Arguments = new string[] { argument };
+
+        /// <summary>Initializes a new instance of the <see cref="InterpolatedStringHandlerArgumentAttribute"/> class.</summary>
+        /// <param name="arguments">The names of the arguments that should be passed to the handler.</param>
+        /// <remarks><see langword="null"/> may be used as the name of the receiver in an instance method.</remarks>
+        public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) => Arguments = arguments;
+
+        /// <summary>Gets the names of the arguments that should be passed to the handler.</summary>
+        /// <remarks><see langword="null"/> may be used as the name of the receiver in an instance method.</remarks>
+        public string[] Arguments { get; }
+    }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringHandlerAttribute.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringHandlerAttribute.cs
new file mode 100644 (file)
index 0000000..76853cc
--- /dev/null
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Runtime.CompilerServices
+{
+    /// <summary>Indicates the attributed type is to be used as an interpolated string handler.</summary>
+    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+    public sealed class InterpolatedStringHandlerAttribute : Attribute
+    {
+        /// <summary>Initializes the <see cref="InterpolatedStringHandlerAttribute"/>.</summary>
+        public InterpolatedStringHandlerAttribute() { }
+    }
+}
index 00af596..6a83271 100644 (file)
@@ -9651,10 +9651,26 @@ namespace System.Runtime.CompilerServices
         public bool AllInternalsVisible { get { throw null; } set { } }
         public string AssemblyName { get { throw null; } }
     }
-    public ref struct InterpolatedStringBuilder
+    [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+    public sealed class InterpolatedStringHandlerAttribute : System.Attribute
+    {
+        public InterpolatedStringHandlerAttribute() { }
+    }
+    [System.AttributeUsage(System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
+    public sealed class InterpolatedStringHandlerArgumentAttribute : System.Attribute
+    {
+        public InterpolatedStringHandlerArgumentAttribute(string argument) { }
+        public InterpolatedStringHandlerArgumentAttribute(params string[] arguments) { }
+        public string[] Arguments { get { throw null; } }
+    }
+    [System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute]
+    public ref struct DefaultInterpolatedStringHandler
     {
         private readonly object _dummy;
         private readonly int _dummyPrimitive;
+        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount) { throw null; }
+        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider) { throw null; }
+        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span<char> initialBuffer) { throw null; }
         public void AppendLiteral(string value) { }
         public void AppendFormatted(System.ReadOnlySpan<char> value) { }
         public void AppendFormatted(System.ReadOnlySpan<char> value, int alignment = 0, string? format = null) { }
@@ -9665,10 +9681,6 @@ namespace System.Runtime.CompilerServices
         public void AppendFormatted(object? value, int alignment = 0, string? format = null) { }
         public void AppendFormatted(string? value) { throw null; }
         public void AppendFormatted(string? value, int alignment = 0, string? format = null) { }
-        public static System.Runtime.CompilerServices.InterpolatedStringBuilder Create(int literalLength, int formattedCount) { throw null; }
-        public static System.Runtime.CompilerServices.InterpolatedStringBuilder Create(int literalLength, int formattedCount, System.IFormatProvider? provider) { throw null; }
-        public static System.Runtime.CompilerServices.InterpolatedStringBuilder Create(int literalLength, int formattedCount, System.Span<char> scratchBuffer) { throw null; }
-        public static System.Runtime.CompilerServices.InterpolatedStringBuilder Create(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span<char> scratchBuffer) { throw null; }
         public override string ToString() { throw null; }
         public string ToStringAndClear() { throw null; }
     }
index 4144333..2a68cc1 100644 (file)
     <Compile Include="System\Runtime\NgenServicingAttributesTests.cs" />
     <Compile Include="System\Runtime\CompilerServices\AttributesTests.cs" />
     <Compile Include="System\Runtime\CompilerServices\ConditionalWeakTableTests.cs" />
+    <Compile Include="System\Runtime\CompilerServices\DefaultInterpolatedStringHandlerTests.cs" />
     <Compile Include="System\Runtime\CompilerServices\FormattableStringFactoryTests.cs" />
-    <Compile Include="System\Runtime\CompilerServices\InterpolatedStringBuilderTests.cs" />
     <Compile Include="System\Runtime\CompilerServices\StrongBoxTests.cs" />
     <Compile Include="System\Runtime\CompilerServices\RuntimeHelpersTests.cs" />
     <Compile Include="System\Runtime\ConstrainedExecution\PrePrepareMethodAttributeTests.cs" />
index ba60939..4e64122 100644 (file)
@@ -295,5 +295,30 @@ namespace System.Runtime.CompilerServices.Tests
         {
             new EnumeratorCancellationAttribute();
         }
+
+        [Fact]
+        public static void InterpolatedStringHandlerAttributeTests()
+        {
+            new InterpolatedStringHandlerAttribute();
+        }
+
+        [Theory]
+        [InlineData("")]
+        [InlineData("param1")]
+        public static void InterpolatedStringHandlerArgumentAttributeTests(string firstParameterName)
+        {
+            var attr1 = new InterpolatedStringHandlerArgumentAttribute(firstParameterName);
+            Assert.NotNull(attr1.Arguments);
+            Assert.Same(attr1.Arguments, attr1.Arguments);
+            Assert.Equal(1, attr1.Arguments.Length);
+            Assert.Equal(firstParameterName, attr1.Arguments[0]);
+
+            string[] arguments = new[] { firstParameterName, "param2" };
+            var attr2 = new InterpolatedStringHandlerArgumentAttribute(arguments);
+            Assert.NotNull(attr2.Arguments);
+            Assert.Same(arguments, attr2.Arguments);
+            Assert.Equal(firstParameterName, attr2.Arguments[0]);
+            Assert.Equal("param2", attr2.Arguments[1]);
+        }
     }
 }
@@ -7,7 +7,7 @@ using Xunit;
 
 namespace System.Runtime.CompilerServices.Tests
 {
-    public class InterpolatedStringBuilderTests
+    public class DefaultInterpolatedStringHandlerTests
     {
         [Theory]
         [InlineData(0, 0)]
@@ -18,51 +18,46 @@ namespace System.Runtime.CompilerServices.Tests
         [InlineData(-16, 1)]
         public void LengthAndHoleArguments_Valid(int literalLength, int formattedCount)
         {
-            InterpolatedStringBuilder.Create(literalLength, formattedCount);
+            new DefaultInterpolatedStringHandler(literalLength, formattedCount);
 
             Span<char> scratch1 = stackalloc char[1];
             foreach (IFormatProvider provider in new IFormatProvider[] { null, new ConcatFormatter(), CultureInfo.InvariantCulture, CultureInfo.CurrentCulture, new CultureInfo("en-US"), new CultureInfo("fr-FR") })
             {
-                InterpolatedStringBuilder.Create(literalLength, formattedCount, provider);
+                new DefaultInterpolatedStringHandler(literalLength, formattedCount, provider);
 
-                InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, default);
-                InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, scratch1);
-                InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, Array.Empty<char>());
-                InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, new char[256]);
+                new DefaultInterpolatedStringHandler(literalLength, formattedCount, provider, default);
+                new DefaultInterpolatedStringHandler(literalLength, formattedCount, provider, scratch1);
+                new DefaultInterpolatedStringHandler(literalLength, formattedCount, provider, Array.Empty<char>());
+                new DefaultInterpolatedStringHandler(literalLength, formattedCount, provider, new char[256]);
             }
-
-            InterpolatedStringBuilder.Create(literalLength, formattedCount, Span<char>.Empty);
-            InterpolatedStringBuilder.Create(literalLength, formattedCount, scratch1);
-            InterpolatedStringBuilder.Create(literalLength, formattedCount, Array.Empty<char>());
-            InterpolatedStringBuilder.Create(literalLength, formattedCount, new char[256]);
         }
 
         [Fact]
         public void ToString_DoesntClear()
         {
-            InterpolatedStringBuilder builder = InterpolatedStringBuilder.Create(0, 0);
-            builder.AppendLiteral("hi");
+            DefaultInterpolatedStringHandler handler = new DefaultInterpolatedStringHandler(0, 0);
+            handler.AppendLiteral("hi");
             for (int i = 0; i < 3; i++)
             {
-                Assert.Equal("hi", builder.ToString());
+                Assert.Equal("hi", handler.ToString());
             }
-            Assert.Equal("hi", builder.ToStringAndClear());
+            Assert.Equal("hi", handler.ToStringAndClear());
         }
 
         [Fact]
         public void ToStringAndClear_Clears()
         {
-            InterpolatedStringBuilder builder = InterpolatedStringBuilder.Create(0, 0);
-            builder.AppendLiteral("hi");
-            Assert.Equal("hi", builder.ToStringAndClear());
-            Assert.Equal(string.Empty, builder.ToStringAndClear());
+            DefaultInterpolatedStringHandler handler = new DefaultInterpolatedStringHandler(0, 0);
+            handler.AppendLiteral("hi");
+            Assert.Equal("hi", handler.ToStringAndClear());
+            Assert.Equal(string.Empty, handler.ToStringAndClear());
         }
 
         [Fact]
         public void AppendLiteral()
         {
             var expected = new StringBuilder();
-            InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0);
+            DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0);
 
             foreach (string s in new[] { "", "a", "bc", "def", "this is a long string", "!" })
             {
@@ -77,7 +72,7 @@ namespace System.Runtime.CompilerServices.Tests
         public void AppendFormatted_ReadOnlySpanChar()
         {
             var expected = new StringBuilder();
-            InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0);
+            DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0);
 
             foreach (string s in new[] { "", "a", "bc", "def", "this is a longer string", "!" })
             {
@@ -108,7 +103,7 @@ namespace System.Runtime.CompilerServices.Tests
         public void AppendFormatted_String()
         {
             var expected = new StringBuilder();
-            InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0);
+            DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0);
 
             foreach (string s in new[] { null, "", "a", "bc", "def", "this is a longer string", "!" })
             {
@@ -141,7 +136,7 @@ namespace System.Runtime.CompilerServices.Tests
             var provider = new ConcatFormatter();
 
             var expected = new StringBuilder();
-            InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0, provider);
+            DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0, provider);
 
             foreach (string s in new[] { null, "", "a" })
             {
@@ -169,7 +164,7 @@ namespace System.Runtime.CompilerServices.Tests
         public void AppendFormatted_ReferenceTypes()
         {
             var expected = new StringBuilder();
-            InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0);
+            DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0);
 
             foreach (string rawInput in new[] { null, "", "a", "bc", "def", "this is a longer string", "!" })
             {
@@ -231,22 +226,22 @@ namespace System.Runtime.CompilerServices.Tests
         public void AppendFormatted_ReferenceTypes_CreateProviderFlowed(bool useScratch)
         {
             var provider = new CultureInfo("en-US");
-            InterpolatedStringBuilder builder = useScratch ?
-                InterpolatedStringBuilder.Create(1, 2, provider, stackalloc char[16]) :
-                InterpolatedStringBuilder.Create(1, 2, provider);
+            DefaultInterpolatedStringHandler handler = useScratch ?
+                new DefaultInterpolatedStringHandler(1, 2, provider, stackalloc char[16]) :
+                new DefaultInterpolatedStringHandler(1, 2, provider);
 
             foreach (IHasToStringState tss in new IHasToStringState[] { new FormattableStringWrapper("hello"), new SpanFormattableStringWrapper("hello") })
             {
-                builder.AppendFormatted(tss);
+                handler.AppendFormatted(tss);
                 Assert.Same(provider, tss.ToStringState.LastProvider);
 
-                builder.AppendFormatted(tss, 1);
+                handler.AppendFormatted(tss, 1);
                 Assert.Same(provider, tss.ToStringState.LastProvider);
 
-                builder.AppendFormatted(tss, "X2");
+                handler.AppendFormatted(tss, "X2");
                 Assert.Same(provider, tss.ToStringState.LastProvider);
 
-                builder.AppendFormatted(tss, 1, "X2");
+                handler.AppendFormatted(tss, 1, "X2");
                 Assert.Same(provider, tss.ToStringState.LastProvider);
             }
         }
@@ -257,7 +252,7 @@ namespace System.Runtime.CompilerServices.Tests
             var provider = new ConcatFormatter();
 
             var expected = new StringBuilder();
-            InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0, provider);
+            DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0, provider);
 
             foreach (string s in new[] { null, "", "a" })
             {
@@ -301,7 +296,7 @@ namespace System.Runtime.CompilerServices.Tests
             void Test<T>(T t)
             {
                 var expected = new StringBuilder();
-                InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0);
+                DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0);
 
                 // struct
                 expected.AppendFormat("{0}", t);
@@ -347,20 +342,20 @@ namespace System.Runtime.CompilerServices.Tests
             void Test<T>(T t)
             {
                 var provider = new CultureInfo("en-US");
-                InterpolatedStringBuilder builder = useScratch ?
-                    InterpolatedStringBuilder.Create(1, 2, provider, stackalloc char[16]) :
-                    InterpolatedStringBuilder.Create(1, 2, provider);
+                DefaultInterpolatedStringHandler handler = useScratch ?
+                    new DefaultInterpolatedStringHandler(1, 2, provider, stackalloc char[16]) :
+                    new DefaultInterpolatedStringHandler(1, 2, provider);
 
-                builder.AppendFormatted(t);
+                handler.AppendFormatted(t);
                 Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider);
 
-                builder.AppendFormatted(t, 1);
+                handler.AppendFormatted(t, 1);
                 Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider);
 
-                builder.AppendFormatted(t, "X2");
+                handler.AppendFormatted(t, "X2");
                 Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider);
 
-                builder.AppendFormatted(t, 1, "X2");
+                handler.AppendFormatted(t, 1, "X2");
                 Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider);
             }
 
@@ -385,7 +380,7 @@ namespace System.Runtime.CompilerServices.Tests
                 }
 
                 var expected = new StringBuilder();
-                InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0, provider);
+                DefaultInterpolatedStringHandler actual = new DefaultInterpolatedStringHandler(0, 0, provider);
 
                 // struct
                 expected.AppendFormat(provider, "{0}", t);
@@ -422,20 +417,20 @@ namespace System.Runtime.CompilerServices.Tests
         public void Grow_Large(bool useScratch)
         {
             var expected = new StringBuilder();
-            InterpolatedStringBuilder builder = useScratch ?
-                InterpolatedStringBuilder.Create(3, 1000, null, stackalloc char[16]) :
-                InterpolatedStringBuilder.Create(3, 1000);
+            DefaultInterpolatedStringHandler handler = useScratch ?
+                new DefaultInterpolatedStringHandler(3, 1000, null, stackalloc char[16]) :
+                new DefaultInterpolatedStringHandler(3, 1000);
 
             for (int i = 0; i < 1000; i++)
             {
-                builder.AppendFormatted(i);
+                handler.AppendFormatted(i);
                 expected.Append(i);
 
-                builder.AppendFormatted(i, 3);
+                handler.AppendFormatted(i, 3);
                 expected.AppendFormat("{0,3}", i);
             }
 
-            Assert.Equal(expected.ToString(), builder.ToStringAndClear());
+            Assert.Equal(expected.ToString(), handler.ToStringAndClear());
         }
 
         private static void AssertModeMatchesType<T>(T tss) where T : IHasToStringState