From 516db737cd41e27830698e538b36d12d70172630 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Thu, 15 Apr 2021 06:22:44 -0400 Subject: [PATCH] Add S.R.CompilerServices.InterpolatedStringBuilder (#51086) * Add InterpolatedStringBuilder * Address PR feedback * Revise based on API review * Address PR feedback * Fix DateOnly/TimeOnly --- .../System.Private.CoreLib.Shared.projitems | 1 + .../Text/Utf8Formatter/Utf8Formatter.Float.cs | 2 +- .../System.Private.CoreLib/src/System/Byte.cs | 2 +- .../System.Private.CoreLib/src/System/Char.cs | 2 + .../src/System/DateOnly.cs | 2 +- .../src/System/DateTime.cs | 2 +- .../src/System/DateTimeOffset.cs | 2 +- .../src/System/Decimal.cs | 2 +- .../src/System/Double.cs | 2 +- .../System.Private.CoreLib/src/System/Guid.cs | 2 +- .../System.Private.CoreLib/src/System/Half.cs | 2 +- .../src/System/ISpanFormattable.cs | 13 +- .../src/System/Int16.cs | 2 +- .../src/System/Int32.cs | 2 +- .../src/System/Int64.cs | 2 +- .../src/System/IntPtr.cs | 2 +- .../InterpolatedStringBuilder.cs | 618 +++++++++++++++++ .../src/System/SByte.cs | 2 +- .../src/System/Single.cs | 2 +- .../src/System/String.cs | 4 + .../src/System/Text/Rune.cs | 2 + .../src/System/Text/StringBuilder.cs | 14 +- .../Text/ValueStringBuilder.AppendFormat.cs | 2 +- .../src/System/TimeOnly.cs | 2 +- .../src/System/TimeSpan.cs | 2 +- .../src/System/UInt16.cs | 2 +- .../src/System/UInt32.cs | 2 +- .../src/System/UInt64.cs | 2 +- .../src/System/UIntPtr.cs | 2 +- .../src/System/Version.cs | 3 + .../ref/System.Runtime.Numerics.cs | 2 +- .../src/System/Numerics/BigInteger.cs | 2 +- .../System.Runtime/ref/System.Runtime.cs | 78 ++- .../tests/System.Runtime.Tests.csproj | 1 + .../InterpolatedStringBuilderTests.cs | 635 ++++++++++++++++++ 35 files changed, 1370 insertions(+), 49 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringBuilder.cs create mode 100644 src/libraries/System.Runtime/tests/System/Runtime/CompilerServices/InterpolatedStringBuilderTests.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index a2bb38f63185c..4ce228dc66a82 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -687,6 +687,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Float.cs b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Float.cs index 6624c3b3e2f11..5a673cb9f0a9e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Float.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/Utf8Formatter/Utf8Formatter.Float.cs @@ -58,7 +58,7 @@ public static bool TryFormat(float value, Span destination, out int bytesW return TryFormatFloatingPoint(value, destination, out bytesWritten, format); } - private static bool TryFormatFloatingPoint(T value, Span destination, out int bytesWritten, StandardFormat format) where T : IFormattable, ISpanFormattable + private static bool TryFormatFloatingPoint(T value, Span destination, out int bytesWritten, StandardFormat format) where T : ISpanFormattable { Span formatText = stackalloc char[0]; diff --git a/src/libraries/System.Private.CoreLib/src/System/Byte.cs b/src/libraries/System.Private.CoreLib/src/System/Byte.cs index 4499bbd14a9f0..8f127b6cbdd46 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Byte.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Byte.cs @@ -12,7 +12,7 @@ namespace System [Serializable] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct Byte : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct Byte : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly byte m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/Char.cs b/src/libraries/System.Private.CoreLib/src/System/Char.cs index c67c02a0c6630..613d0909429d4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Char.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Char.cs @@ -179,6 +179,8 @@ bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, Re return false; } + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(m_value); + public static char Parse(string s) { if (s == null) diff --git a/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs index 6cee176a31d10..6e143a925be8c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs @@ -18,7 +18,7 @@ internal enum ParseOperationResult /// /// Represents dates with values ranging from January 1, 0001 Anno Domini (Common Era) through December 31, 9999 A.D. (C.E.) in the Gregorian calendar. /// - public readonly struct DateOnly : IComparable, IComparable, IEquatable, IFormattable, ISpanFormattable + public readonly struct DateOnly : IComparable, IComparable, IEquatable, ISpanFormattable { private readonly int _dayNumber; diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs index aee82eb98c608..dc2ebdf1380df 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs @@ -43,7 +43,7 @@ namespace System [StructLayout(LayoutKind.Auto)] [Serializable] [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly partial struct DateTime : IComparable, IFormattable, IConvertible, IComparable, IEquatable, ISerializable, ISpanFormattable + public readonly partial struct DateTime : IComparable, ISpanFormattable, IConvertible, IComparable, IEquatable, ISerializable { // Number of 100ns ticks per time unit private const long TicksPerMillisecond = 10000; diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs index 5b5fbd044919a..8b91fd6a2a8f3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs @@ -31,7 +31,7 @@ namespace System [StructLayout(LayoutKind.Auto)] [Serializable] [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct DateTimeOffset : IComparable, IFormattable, IComparable, IEquatable, ISerializable, IDeserializationCallback, ISpanFormattable + public readonly struct DateTimeOffset : IComparable, ISpanFormattable, IComparable, IEquatable, ISerializable, IDeserializationCallback { // Constants internal const long MaxOffset = TimeSpan.TicksPerHour * 14; diff --git a/src/libraries/System.Private.CoreLib/src/System/Decimal.cs b/src/libraries/System.Private.CoreLib/src/System/Decimal.cs index 04fffff08ce68..429d4c3cdf6a9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Decimal.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Decimal.cs @@ -57,7 +57,7 @@ namespace System [Serializable] [System.Runtime.Versioning.NonVersionable] // This only applies to field layout [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly partial struct Decimal : IFormattable, IComparable, IConvertible, IComparable, IEquatable, ISpanFormattable, ISerializable, IDeserializationCallback + public readonly partial struct Decimal : ISpanFormattable, IComparable, IConvertible, IComparable, IEquatable, ISerializable, IDeserializationCallback { // Sign mask for the flags field. A value of zero in this bit indicates a // positive Decimal value, and a value of one in this bit indicates a diff --git a/src/libraries/System.Private.CoreLib/src/System/Double.cs b/src/libraries/System.Private.CoreLib/src/System/Double.cs index 912d19e1d7222..d3c9741202751 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Double.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Double.cs @@ -24,7 +24,7 @@ namespace System [Serializable] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct Double : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct Double : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly double m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/Guid.cs b/src/libraries/System.Private.CoreLib/src/System/Guid.cs index 19619cccc337e..0a26175427939 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Guid.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Guid.cs @@ -18,7 +18,7 @@ namespace System [Serializable] [NonVersionable] // This only applies to field layout [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly partial struct Guid : IFormattable, IComparable, IComparable, IEquatable, ISpanFormattable + public readonly partial struct Guid : ISpanFormattable, IComparable, IComparable, IEquatable { public static readonly Guid Empty; diff --git a/src/libraries/System.Private.CoreLib/src/System/Half.cs b/src/libraries/System.Private.CoreLib/src/System/Half.cs index 92ae792c854a7..63d81ec68af8b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Half.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Half.cs @@ -15,7 +15,7 @@ namespace System /// An IEEE 754 compliant float16 type. /// [StructLayout(LayoutKind.Sequential)] - public readonly struct Half : IComparable, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct Half : IComparable, ISpanFormattable, IComparable, IEquatable { private const NumberStyles DefaultParseStyle = NumberStyles.Float | NumberStyles.AllowThousands; diff --git a/src/libraries/System.Private.CoreLib/src/System/ISpanFormattable.cs b/src/libraries/System.Private.CoreLib/src/System/ISpanFormattable.cs index 3c8fb80ccf554..0998785f780ba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/ISpanFormattable.cs +++ b/src/libraries/System.Private.CoreLib/src/System/ISpanFormattable.cs @@ -3,8 +3,19 @@ namespace System { - internal interface ISpanFormattable + /// Provides functionality to format the string representation of an object into a span. + public interface ISpanFormattable : IFormattable { + /// Tries to format the value of the current instance into the provided span of characters. + /// When this method returns, this instance's value formatted as a span of characters. + /// When this method returns, the number of characters that were written in . + /// A span containing the characters that represent a standard or custom format string that defines the acceptable format for . + /// An optional object that supplies culture-specific formatting information for . + /// if the formatting was successful; otherwise, . + /// + /// An implementation of this interface should produce the same string of characters as an implementation of + /// on the same type. + /// bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Int16.cs b/src/libraries/System.Private.CoreLib/src/System/Int16.cs index 72278e5b5447b..8f33d790699e9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Int16.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Int16.cs @@ -12,7 +12,7 @@ namespace System [Serializable] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct Int16 : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct Int16 : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly short m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/Int32.cs b/src/libraries/System.Private.CoreLib/src/System/Int32.cs index 213624a8fe515..14822bac0d6b1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Int32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Int32.cs @@ -12,7 +12,7 @@ namespace System [Serializable] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct Int32 : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct Int32 : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly int m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/Int64.cs b/src/libraries/System.Private.CoreLib/src/System/Int64.cs index 0a8796e58f1e1..2c53290de5976 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Int64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Int64.cs @@ -12,7 +12,7 @@ namespace System [Serializable] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct Int64 : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct Int64 : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly long m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/IntPtr.cs b/src/libraries/System.Private.CoreLib/src/System/IntPtr.cs index 01229fff69b3d..16735ffc4210a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IntPtr.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IntPtr.cs @@ -21,7 +21,7 @@ namespace System [Serializable] [StructLayout(LayoutKind.Sequential)] [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct IntPtr : IEquatable, IComparable, IComparable, IFormattable, ISpanFormattable, ISerializable + public readonly struct IntPtr : IEquatable, IComparable, IComparable, ISpanFormattable, ISerializable { // WARNING: We allow diagnostic tools to directly inspect this member (_value). // See https://github.com/dotnet/corert/blob/master/Documentation/design-docs/diagnostics/diagnostics-tools-contract.md for more details. diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringBuilder.cs new file mode 100644 index 0000000000000..22b781004436a --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringBuilder.cs @@ -0,0 +1,618 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; + +namespace System.Runtime.CompilerServices +{ + /// Provides a builder used by the language compiler to process interpolated strings into instances. + public ref struct InterpolatedStringBuilder + { + // 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. + + /// Expected average length of formatted data used for an individual hole. + /// + /// 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}", + /// since the compiler-provided base length won't include the equivalent character count. + /// + private const int GuessedLengthPerHole = 11; + /// Minimum size array to rent from the pool. + /// Same as stack-allocation size used today by string.Format. + private const int MinimumArrayPoolLength = 256; + + /// Optional provider to pass to IFormattable.ToString or ISpanFormattable.TryFormat calls. + private readonly IFormatProvider? _provider; + /// Optional custom formatter derived from the . + private readonly object? _customFormatter; + /// Array rented from the array pool and used to back . + private char[]? _arrayToReturnToPool; + /// The span to write into. + private Span _chars; + /// Position at which to write the next character. + private int _pos; + + /// Initializes the builder. + /// Approximated capacity required to support the interpolated string. The final size may be smaller or larger. + private InterpolatedStringBuilder(int initialCapacity) + { + _provider = null; + _customFormatter = null; + _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _pos = 0; + } + + /// Initializes the builder. + /// Approximated capacity required to support the interpolated string. The final size may be smaller or larger. + /// An object that supplies culture-specific formatting information. + private InterpolatedStringBuilder(int initialCapacity, IFormatProvider? provider) + { + _provider = provider; + _customFormatter = provider?.GetFormat(typeof(ICustomFormatter)); + _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _pos = 0; + } + + /// Initializes the builder. + /// A buffer temporarily transferred to the builder for use as part of its formatting. Contents may be overwritten. + private InterpolatedStringBuilder(Span scratchBuffer) + { + _provider = null; + _customFormatter = null; + _arrayToReturnToPool = null; + _chars = scratchBuffer; + _pos = 0; + } + + /// Initializes the builder. + /// A buffer temporarily transferred to the builder for use as part of its formatting. Contents may be overwritten. + /// An object that supplies culture-specific formatting information. + private InterpolatedStringBuilder(Span scratchBuffer, IFormatProvider? provider) + { + _provider = provider; + _customFormatter = provider?.GetFormat(typeof(ICustomFormatter)); + _arrayToReturnToPool = null; + _chars = scratchBuffer; + _pos = 0; + } + + /// Creates a builder used to translate an interpolated string into a . + /// The number of constant characters outside of holes in the interpolated string. + /// The number of holes in the interpolated string. + /// 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. + public static InterpolatedStringBuilder Create(int literalLength, int formattedCount) => + new InterpolatedStringBuilder(GetDefaultLength(literalLength, formattedCount)); + + /// Creates a builder used to translate an interpolated string into a . + /// The number of constant characters outside of holes in the interpolated string. + /// The number of holes in the interpolated string. + /// An object that supplies culture-specific formatting information. + /// 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. + public static InterpolatedStringBuilder Create(int literalLength, int formattedCount, IFormatProvider? provider) => + new InterpolatedStringBuilder(GetDefaultLength(literalLength, formattedCount), provider); + + /// Creates a builder used to translate an interpolated string into a . + /// The number of constant characters outside of holes in the interpolated string. + /// The number of holes in the interpolated string. + /// A buffer temporarily transferred to the builder for use as part of its formatting. Contents may be overwritten. + /// 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. + public static InterpolatedStringBuilder Create(int literalLength, int formattedCount, Span scratchBuffer) => + new InterpolatedStringBuilder(scratchBuffer); + + /// Creates a builder used to translate an interpolated string into a . + /// The number of constant characters outside of holes in the interpolated string. + /// The number of holes in the interpolated string. + /// An object that supplies culture-specific formatting information. + /// A buffer temporarily transferred to the builder for use as part of its formatting. Contents may be overwritten. + /// 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. + public static InterpolatedStringBuilder Create(int literalLength, int formattedCount, IFormatProvider? provider, Span scratchBuffer) => + new InterpolatedStringBuilder(scratchBuffer, provider); + + /// Derives a default length with which to seed the builder. + /// The number of constant characters outside of holes in the interpolated string. + /// The number of holes in the interpolated string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetDefaultLength(int literalLength, int formattedCount) => + Math.Max(MinimumArrayPoolLength, literalLength + (formattedCount * GuessedLengthPerHole)); + + /// Gets the built . + /// The built string. + public override string ToString() => _chars.Slice(0, _pos).ToString(); + + /// Gets the built and clears the builder. + /// The built string. + /// + /// 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 + /// is called on any one of them. + /// + public string ToStringAndClear() + { + string result = _chars.Slice(0, _pos).ToString(); + + char[]? toReturn = _arrayToReturnToPool; + this = default; // defensive clear + if (toReturn is not null) + { + ArrayPool.Shared.Return(toReturn); + } + + return result; + } + + /// Writes the specified string to the builder. + /// The string to write. + public void AppendLiteral(string value) + { + if (value.TryCopyTo(_chars.Slice(_pos))) + { + _pos += value.Length; + } + else + { + GrowThenCopyString(value); + } + } + + #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 + // 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.) + // + // At a minimum, then, we would need an overload that accepts: + // (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 could not be used in holes. + // - 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. + // + // To address that, we could just have overloads for T and ReadOnlySpan: + // (T) + // (T, int alignment) + // (T, string? format) + // (T, int alignment, string? format) + // (ReadOnlySpan) + // (ReadOnlySpan, int alignment) + // (ReadOnlySpan, string? format) + // (ReadOnlySpan, 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 + // 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 + // from doing so. + // - Nullable value types also pay a heavy price, in particular around interface checks that would generally evaporate + // at compile time for value types but don't (currently) if the Nullable 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 + // 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 + // (object, ...) + // (ReadOnlySpan, ...) + // (string, ...) + // but this also has shortcomings, most importantly: + // - If you have an unconstrained T that happens to be a value type, it'll now end up getting boxed to use the object overload. + // This also necessitates the T? overload, since nullable value types don't meet a T : struct constraint, so without those + // they'd all map to the object overloads as well. + // - Any reference type with an implicit cast to ROS 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: + // (T, ...) with no constraint + // (ReadOnlySpan) and (ReadOnlySpan, int) + // (object, int alignment = 0, string? format = null) + // (string) and (string, int) + // This would address most of the concerns, at the expense of: + // - Most reference types going through the generic code paths and so being a bit more expensive. + // - Nullable types being more expensive until https://github.com/dotnet/runtime/issues/50915 is addressed. + // We could choose to add a T? where T : struct set of overloads if necessary. + // Strings don't require their own overloads here, but as they're expected to be very common and as we can + // optimize them in several ways (can copy the contents directly, don't need to do any interface checks, don't + // need to pay the shared generic overheads, etc.) we can add overloads specifically to optimize for them. + // + // Hole values are formatted according to the following policy: + // 1. If an IFormatProvider was supplied and it provides an ICustomFormatter, use ICustomFormatter.Format (even if the value is null). + // 2. If the type implements ISpanFormattable, use ISpanFormattable.TryFormat. + // 3. If the type implements IFormattable, use IFormattable.ToString. + // 4. Otherwise, use object.ToString. + // This matches the behavior of string.Format, StringBuilder.AppendFormat, etc. The only overloads for which this doesn't + // apply is ReadOnlySpan, which isn't supported by either string.Format nor StringBuilder.AppendFormat, but more + // importantly which can't be boxed to be passed to ICustomFormatter.Format. + + #region AppendFormatted T + /// Writes the specified value to the builder. + /// The value to write. + public void AppendFormatted(T value) + { + // This method could delegate to AppendFormatted with a null format, but explicitly passing + // default as the format to TryFormat helps to improve code quality in some cases when TryFormat is inlined, + // e.g. for Int32 it enables the JIT to eliminate code in the inlined method based on a length check on the format. + + // If there's a custom formatter, always use it. + if (_customFormatter is not null) + { + AppendCustomFormatter(value, format: null); + return; + } + + // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter + // requires the former. For value types, it won't matter as the type checks devolve into + // JIT-time constants. For reference types, they're more likely to implement IFormattable + // than they are to implement ISpanFormattable: if they don't implement either, we save an + // interface check over first checking for ISpanFormattable and then for IFormattable, and + // if it only implements IFormattable, we come out even: only if it implements both do we + // end up paying for an extra interface check. + string? s; + if (value is IFormattable) + { + // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) + { + int charsWritten; + while (!((ISpanFormattable)value).TryFormat(_chars.Slice(_pos), out charsWritten, default, _provider)) // constrained call avoiding boxing for value types + { + Grow(); + } + + _pos += charsWritten; + return; + } + + s = ((IFormattable)value).ToString(format: null, _provider); // constrained call avoiding boxing for value types + } + else + { + s = value?.ToString(); + } + + if (s is not null) + { + AppendLiteral(s); + } + } + /// Writes the specified value to the builder. + /// The value to write. + /// The format string. + public void AppendFormatted(T value, string? format) + { + // If there's a custom formatter, always use it. + if (_customFormatter is not null) + { + AppendCustomFormatter(value, format); + return; + } + + // Check first for IFormattable, even though we'll prefer to use ISpanFormattable, as the latter + // requires the former. For value types, it won't matter as the type checks devolve into + // JIT-time constants. For reference types, they're more likely to implement IFormattable + // than they are to implement ISpanFormattable: if they don't implement either, we save an + // interface check over first checking for ISpanFormattable and then for IFormattable, and + // if it only implements IFormattable, we come out even: only if it implements both do we + // end up paying for an extra interface check. + string? s; + if (value is IFormattable) + { + // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) + { + int charsWritten; + while (!((ISpanFormattable)value).TryFormat(_chars.Slice(_pos), out charsWritten, format, _provider)) // constrained call avoiding boxing for value types + { + Grow(); + } + + _pos += charsWritten; + return; + } + + s = ((IFormattable)value).ToString(format, _provider); // constrained call avoiding boxing for value types + } + else + { + s = value?.ToString(); + } + + if (s is not null) + { + AppendLiteral(s); + } + } + + /// Writes the specified value to the builder. + /// The value to write. + /// 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. + public void AppendFormatted(T value, int alignment) + { + int startingPos = _pos; + AppendFormatted(value); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + + /// Writes the specified value to the builder. + /// The value to write. + /// The format string. + /// 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. + public void AppendFormatted(T value, int alignment, string? format) + { + int startingPos = _pos; + AppendFormatted(value, format); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + #endregion + + #region AppendFormatted ReadOnlySpan + /// Writes the specified character span to the builder. + /// The span to write. + public void AppendFormatted(ReadOnlySpan value) + { + // Fast path for when the value fits in the current buffer + if (value.TryCopyTo(_chars.Slice(_pos))) + { + _pos += value.Length; + } + else + { + GrowThenCopySpan(value); + } + } + + /// Writes the specified string of chars to the builder. + /// The span to write. + /// 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. + /// The format string. + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) + { + bool leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + int paddingRequired = alignment - value.Length; + if (paddingRequired <= 0) + { + // The value is as large or larger than the required amount of padding, + // so just write the value. + AppendFormatted(value); + return; + } + + // Write the value along with the appropriate padding. + EnsureCapacityForAdditionalChars(value.Length + paddingRequired); + if (leftAlign) + { + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + _chars.Slice(_pos, paddingRequired).Fill(' '); + _pos += paddingRequired; + } + else + { + _chars.Slice(_pos, paddingRequired).Fill(' '); + _pos += paddingRequired; + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + } + #endregion + + #region AppendFormatted string + /// Writes the specified value to the builder. + /// The value to write. + public void AppendFormatted(string? value) + { + // Fast-path for no custom formatter and a non-null string that fits in the current destination buffer. + if (_customFormatter is null && + value is not null && + value.TryCopyTo(_chars.Slice(_pos))) + { + _pos += value.Length; + } + else + { + AppendFormattedSlow(value); + } + } + + /// Writes the specified value to the builder. + /// The value to write. + /// + /// Slow path to handle a custom formatter, potentially null value, + /// or a string that doesn't fit in the current buffer. + /// + [MethodImpl(MethodImplOptions.NoInlining)] + private void AppendFormattedSlow(string? value) + { + if (_customFormatter is null) + { + if (value is not null) + { + EnsureCapacityForAdditionalChars(value.Length); + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + } + else + { + AppendCustomFormatter(value, format: null); + } + } + + /// Writes the specified value to the builder. + /// The value to write. + /// 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. + /// The format string. + public void AppendFormatted(string? value, int alignment = 0, string? format = null) => + // Format is meaningless for strings and doesn't make sense for someone to specify. We have the overload + // simply to disambiguate between ROS and object, just in case someone does specify a format, as + // string is implicitly convertible to both. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + #endregion + + #region AppendFormatted object + /// Writes the specified value to the builder. + /// The value to write. + /// 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. + /// The format string. + public void AppendFormatted(object? value, int alignment = 0, string? format = null) => + // This overload is expected to be used rarely, only if either a) something strongly typed as object is + // formatted with both an alignment and a format, or b) the compiler is unable to target type to T. It + // exists purely to help make cases from (b) compile. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + #endregion + #endregion + + /// Formats the value using the custom formatter from the provider. + /// The value to write. + /// The format string. + [MethodImpl(MethodImplOptions.NoInlining)] + private void AppendCustomFormatter(T value, string? format) + { + // This case is very rare, but we need to handle it prior to the other checks in case + // a provider was used that supplied an ICustomFormatter which wanted to intercept the particular value. + // We do the cast here rather than in the ctor, even though this could be executed multiple times per + // formatting, to make the cast pay for play. + Debug.Assert(_customFormatter is not null); + if (((ICustomFormatter)_customFormatter).Format(format, value, _provider) is string customFormatted) + { + AppendLiteral(customFormatted); + } + } + + /// Handles adding any padding required for aligning a formatted value in a hole. + /// The position at which the written value started. + /// 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. + private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment) + { + Debug.Assert(startingPos >= 0 && startingPos <= _pos); + Debug.Assert(alignment != 0); + + int charsWritten = _pos - startingPos; + + bool leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + int paddingNeeded = alignment - charsWritten; + if (paddingNeeded > 0) + { + EnsureCapacityForAdditionalChars(paddingNeeded); + + if (leftAlign) + { + _chars.Slice(_pos, paddingNeeded).Fill(' '); + } + else + { + _chars.Slice(startingPos, charsWritten).CopyTo(_chars.Slice(startingPos + paddingNeeded)); + _chars.Slice(startingPos, paddingNeeded).Fill(' '); + } + + _pos += paddingNeeded; + } + } + + /// Ensures has the capacity to store beyond . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacityForAdditionalChars(int additionalChars) + { + if (_chars.Length - _pos < additionalChars) + { + Grow(additionalChars); + } + } + + /// Fallback for fast path in when there's not enough space in the destination. + /// The string to write. + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowThenCopyString(string value) + { + Grow(value.Length); + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + /// Fallback for for when not enough space exists in the current buffer. + /// The span to write. + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowThenCopySpan(ReadOnlySpan value) + { + Grow(value.Length); + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + /// Grows to have the capacity to store at least beyond . + [MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible + private void Grow(int additionalChars) + { + // This method is called when the remaining space (_chars.Length - _pos) is + // insufficient to store a specific number of additional characters. Thus, we + // need to grow to at least that new total. GrowCore will handle growing by more + // than that if possible. + Debug.Assert(additionalChars > _chars.Length - _pos); + GrowCore((uint)_pos + (uint)additionalChars); + } + + /// Grows the size of . + [MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible + private void Grow() + { + // This method is called when the remaining space in _chars isn't sufficient to continue + // the operation. Thus, we need at least one character beyond _chars.Length. GrowCore + // will handle growing by more than that if possible. + GrowCore((uint)_chars.Length + 1); + } + + /// Grow the size of to at least the specified . + [MethodImpl(MethodImplOptions.AggressiveInlining)] // but reuse this grow logic directly in both of the above grow routines + private void GrowCore(uint requiredMinCapacity) + { + // We want the max of how much space we actually required and doubling our capacity (without going beyond the max allowed length). We + // also want to avoid asking for small arrays, to reduce the number of times we need to grow, and since we're working with unsigned + // ints that could technically overflow if someone tried to, for example, append a huge string to a huge string, we also clamp to int.MaxValue. + // Even if the array creation fails in such a case, we may later fail in ToStringAndClear. + + uint newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, string.MaxLength)); + int arraySize = (int)Math.Clamp(newCapacity, MinimumArrayPoolLength, int.MaxValue); + + char[] newArray = ArrayPool.Shared.Rent(arraySize); + _chars.Slice(0, _pos).CopyTo(newArray); + + char[]? toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = newArray; + + if (toReturn is not null) + { + ArrayPool.Shared.Return(toReturn); + } + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/SByte.cs b/src/libraries/System.Private.CoreLib/src/System/SByte.cs index 24f3d5a640908..880492a459757 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SByte.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SByte.cs @@ -13,7 +13,7 @@ namespace System [CLSCompliant(false)] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct SByte : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct SByte : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly sbyte m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/Single.cs b/src/libraries/System.Private.CoreLib/src/System/Single.cs index 2d9993203f96c..8ba2e6e1496dc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Single.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Single.cs @@ -23,7 +23,7 @@ namespace System [Serializable] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct Single : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct Single : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly float m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/String.cs b/src/libraries/System.Private.CoreLib/src/System/String.cs index 30d7a6e8021b1..858d59fbaae57 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.cs @@ -24,6 +24,10 @@ namespace System [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable, IComparable, IEquatable, ICloneable { + /// Maximum length allowed for a string. + /// Keep in sync with AllocateString in gchelpers.cpp. + internal const int MaxLength = 0x3FFFFFDF; + // // These fields map directly onto the fields in an EE StringObject. See object.h for the layout. // diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs index c8373167b60ca..7678b5e480267 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs @@ -919,6 +919,8 @@ public override string ToString() #if SYSTEM_PRIVATE_CORELIB bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => TryEncodeToUtf16(destination, out charsWritten); + + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); #endif /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs index 820df398dd088..3ff675c1d40a6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs @@ -1174,6 +1174,8 @@ public StringBuilder Append(char value) private StringBuilder AppendSpanFormattable(T value) where T : ISpanFormattable { + Debug.Assert(typeof(T).Assembly.Equals(typeof(object).Assembly), "Implementation trusts the results of TryFormat because T is expected to be something known"); + if (value.TryFormat(RemainingCurrentChunk, out int charsWritten, format: default, provider: null)) { m_ChunkLength += charsWritten; @@ -1183,8 +1185,10 @@ public StringBuilder Append(char value) return Append(value.ToString()); } - internal StringBuilder AppendSpanFormattable(T value, string? format, IFormatProvider? provider) where T : ISpanFormattable, IFormattable + internal StringBuilder AppendSpanFormattable(T value, string? format, IFormatProvider? provider) where T : ISpanFormattable { + Debug.Assert(typeof(T).Assembly.Equals(typeof(object).Assembly), "Implementation trusts the results of TryFormat because T is expected to be something known"); + if (value.TryFormat(RemainingCurrentChunk, out int charsWritten, format, provider)) { m_ChunkLength += charsWritten; @@ -1746,6 +1750,14 @@ internal StringBuilder AppendFormatHelper(IFormatProvider? provider, string form (leftJustify || width == 0) && spanFormattableArg.TryFormat(RemainingCurrentChunk, out int charsWritten, itemFormatSpan, provider)) { + if ((uint)charsWritten > (uint)RemainingCurrentChunk.Length) + { + // Untrusted ISpanFormattable implementations might return an erroneous charsWritten value, + // and m_ChunkLength might end up being used in unsafe code, so fail if we get back an + // out-of-range charsWritten value. + FormatError(); + } + m_ChunkLength += charsWritten; // Pad the end, if needed. diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs b/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs index 9927455bd6ba1..d11eae6ce47b9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/ValueStringBuilder.AppendFormat.cs @@ -43,7 +43,7 @@ public void AppendFormat(IFormatProvider? provider, string format, params object AppendFormatHelper(provider, format, new ParamsArray(args)); } - internal void AppendSpanFormattable(T value, string? format, IFormatProvider? provider) where T : ISpanFormattable, IFormattable + internal void AppendSpanFormattable(T value, string? format, IFormatProvider? provider) where T : ISpanFormattable { if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider)) { diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs index 291bc47f73d56..d42ce995d28a3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs @@ -9,7 +9,7 @@ namespace System /// /// Represents a time of day, as would be read from a clock, within the range 00:00:00 to 23:59:59.9999999. /// - public readonly struct TimeOnly : IComparable, IComparable, IEquatable, IFormattable, ISpanFormattable + public readonly struct TimeOnly : IComparable, IComparable, IEquatable, ISpanFormattable { // represent the number of ticks map to the time of the day. 1 ticks = 100-nanosecond in time measurements. private readonly long _ticks; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs b/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs index d0d03c0dfe5de..b0634677650b4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeSpan.cs @@ -25,7 +25,7 @@ namespace System // an appropriate custom ILMarshaler to keep WInRT interop scenarios enabled. // [Serializable] - public readonly struct TimeSpan : IComparable, IComparable, IEquatable, IFormattable, ISpanFormattable + public readonly struct TimeSpan : IComparable, IComparable, IEquatable, ISpanFormattable { public const long TicksPerMillisecond = 10000; diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt16.cs b/src/libraries/System.Private.CoreLib/src/System/UInt16.cs index b16a3b6e2543f..35dff0a957a0d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt16.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt16.cs @@ -13,7 +13,7 @@ namespace System [CLSCompliant(false)] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct UInt16 : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct UInt16 : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly ushort m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt32.cs b/src/libraries/System.Private.CoreLib/src/System/UInt32.cs index c5949d52ea54f..fd1f741345fe7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt32.cs @@ -13,7 +13,7 @@ namespace System [CLSCompliant(false)] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct UInt32 : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct UInt32 : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly uint m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/UInt64.cs b/src/libraries/System.Private.CoreLib/src/System/UInt64.cs index 5b9ad1de4f84b..da6773eaa8aba 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UInt64.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UInt64.cs @@ -13,7 +13,7 @@ namespace System [CLSCompliant(false)] [StructLayout(LayoutKind.Sequential)] [TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct UInt64 : IComparable, IConvertible, IFormattable, IComparable, IEquatable, ISpanFormattable + public readonly struct UInt64 : IComparable, IConvertible, ISpanFormattable, IComparable, IEquatable { private readonly ulong m_value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/UIntPtr.cs b/src/libraries/System.Private.CoreLib/src/System/UIntPtr.cs index c03e47fcf3fcc..dcb2ce79c9c2f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/UIntPtr.cs +++ b/src/libraries/System.Private.CoreLib/src/System/UIntPtr.cs @@ -22,7 +22,7 @@ namespace System [CLSCompliant(false)] [StructLayout(LayoutKind.Sequential)] [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")] - public readonly struct UIntPtr : IEquatable, IComparable, IComparable, IFormattable, ISpanFormattable, ISerializable + public readonly struct UIntPtr : IEquatable, IComparable, IComparable, ISpanFormattable, ISerializable { private readonly unsafe void* _value; // Do not rename (binary serialization) diff --git a/src/libraries/System.Private.CoreLib/src/System/Version.cs b/src/libraries/System.Private.CoreLib/src/System/Version.cs index 61aee27ddf1c7..455978045f3c7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Version.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Version.cs @@ -189,6 +189,9 @@ public string ToString(int fieldCount) return dest.Slice(0, charsWritten).ToString(); } + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => + ToString(); + public bool TryFormat(Span destination, out int charsWritten) => TryFormat(destination, DefaultFormatFieldCount, out charsWritten); diff --git a/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs b/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs index fcb447ecf34f5..4c075047d9b64 100644 --- a/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs +++ b/src/libraries/System.Runtime.Numerics/ref/System.Runtime.Numerics.cs @@ -6,7 +6,7 @@ namespace System.Numerics { - public readonly partial struct BigInteger : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + public readonly partial struct BigInteger : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { private readonly object _dummy; private readonly int _dummyPrimitive; diff --git a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs index 58ac3ee501202..5edffac96d9bf 100644 --- a/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs +++ b/src/libraries/System.Runtime.Numerics/src/System/Numerics/BigInteger.cs @@ -9,7 +9,7 @@ namespace System.Numerics { [Serializable] [System.Runtime.CompilerServices.TypeForwardedFrom("System.Numerics, Version=4.0.0.0, PublicKeyToken=b77a5c561934e089")] - public readonly struct BigInteger : IFormattable, IComparable, IComparable, IEquatable + public readonly struct BigInteger : ISpanFormattable, IComparable, IComparable, IEquatable { private const uint kuMaskHighBit = unchecked((uint)int.MinValue); private const int kcbitUint = 32; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 4c9670bea6696..c418d29508a2e 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -683,7 +683,7 @@ public static partial class Buffer public unsafe static void MemoryCopy(void* source, void* destination, ulong destinationSizeInBytes, ulong sourceBytesToCopy) { } public static void SetByte(System.Array array, int index, byte value) { } } - public readonly partial struct Byte : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct Byte : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly byte _dummyPrimitive; public const byte MaxValue = (byte)255; @@ -731,7 +731,7 @@ public partial class CannotUnloadAppDomainException : System.SystemException public CannotUnloadAppDomainException(string? message) { } public CannotUnloadAppDomainException(string? message, System.Exception? innerException) { } } - public readonly partial struct Char : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable + public readonly partial struct Char : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly char _dummyPrimitive; public const char MaxValue = '\uFFFF'; @@ -806,6 +806,8 @@ public partial class CannotUnloadAppDomainException : System.SystemException public static System.Char ToUpper(System.Char c, System.Globalization.CultureInfo culture) { throw null; } public static System.Char ToUpperInvariant(System.Char c) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, out System.Char result) { throw null; } + bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + string System.IFormattable.ToString(string? format, IFormatProvider? formatProvider) { throw null; } } public sealed partial class CharEnumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.ICloneable, System.IDisposable { @@ -1294,7 +1296,7 @@ public static partial class Convert public static bool TryToBase64Chars(System.ReadOnlySpan bytes, System.Span chars, out int charsWritten, System.Base64FormattingOptions options = System.Base64FormattingOptions.None) { throw null; } } public delegate TOutput Converter(TInput input); - public readonly struct DateOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + public readonly struct DateOnly : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { public static DateOnly MinValue { get { throw null; } } public static DateOnly MaxValue { get { throw null; } } @@ -1354,7 +1356,7 @@ public static partial class Convert public string ToString(string? format, System.IFormatProvider? provider) { throw null; } public bool TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format = default(System.ReadOnlySpan), System.IFormatProvider? provider = null) { throw null; } } - public readonly partial struct DateTime : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable, System.Runtime.Serialization.ISerializable + public readonly partial struct DateTime : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable, System.Runtime.Serialization.ISerializable { private readonly int _dummyPrimitive; public static readonly System.DateTime MaxValue; @@ -1482,7 +1484,7 @@ public enum DateTimeKind Utc = 1, Local = 2, } - public readonly partial struct DateTimeOffset : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable + public readonly partial struct DateTimeOffset : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable { private readonly int _dummyPrimitive; public static readonly System.DateTimeOffset MaxValue; @@ -1610,7 +1612,7 @@ public sealed partial class DBNull : System.IConvertible, System.Runtime.Seriali public override string ToString() { throw null; } public string ToString(System.IFormatProvider? provider) { throw null; } } - public readonly partial struct Decimal : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable + public readonly partial struct Decimal : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable { private readonly int _dummyPrimitive; [System.Runtime.CompilerServices.DecimalConstantAttribute((byte)0, (byte)0, (uint)4294967295, (uint)4294967295, (uint)4294967295)] @@ -1797,7 +1799,7 @@ public partial class DivideByZeroException : System.ArithmeticException public DivideByZeroException(string? message) { } public DivideByZeroException(string? message, System.Exception? innerException) { } } - public readonly partial struct Double : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct Double : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly double _dummyPrimitive; public const double Epsilon = 5E-324; @@ -2245,7 +2247,7 @@ public partial class GopherStyleUriParser : System.UriParser { public GopherStyleUriParser() { } } - public readonly partial struct Guid : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + public readonly partial struct Guid : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { private readonly int _dummyPrimitive; public static readonly System.Guid Empty; @@ -2278,8 +2280,9 @@ public partial class GopherStyleUriParser : System.UriParser public static bool TryParseExact(System.ReadOnlySpan input, System.ReadOnlySpan format, out System.Guid result) { throw null; } public static bool TryParseExact([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? format, out System.Guid result) { throw null; } public bool TryWriteBytes(System.Span destination) { throw null; } + bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } } - public readonly partial struct Half : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + public readonly partial struct Half : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { private readonly int _dummyPrimitive; public static System.Half Epsilon { get { throw null; } } @@ -2450,7 +2453,7 @@ public sealed partial class InsufficientMemoryException : System.OutOfMemoryExce public InsufficientMemoryException(string? message) { } public InsufficientMemoryException(string? message, System.Exception? innerException) { } } - public readonly partial struct Int16 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct Int16 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly short _dummyPrimitive; public const short MaxValue = (short)32767; @@ -2491,7 +2494,7 @@ public sealed partial class InsufficientMemoryException : System.OutOfMemoryExce public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Int16 result) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, out System.Int16 result) { throw null; } } - public readonly partial struct Int32 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct Int32 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly int _dummyPrimitive; public const int MaxValue = 2147483647; @@ -2532,7 +2535,7 @@ public sealed partial class InsufficientMemoryException : System.OutOfMemoryExce public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Int32 result) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, out System.Int32 result) { throw null; } } - public readonly partial struct Int64 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct Int64 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly long _dummyPrimitive; public const long MaxValue = (long)9223372036854775807; @@ -2573,7 +2576,7 @@ public sealed partial class InsufficientMemoryException : System.OutOfMemoryExce public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.Globalization.NumberStyles style, System.IFormatProvider? provider, out System.Int64 result) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, out System.Int64 result) { throw null; } } - public readonly partial struct IntPtr : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.Runtime.Serialization.ISerializable + public readonly partial struct IntPtr : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable, System.Runtime.Serialization.ISerializable { private readonly int _dummyPrimitive; public static readonly System.IntPtr Zero; @@ -2665,6 +2668,10 @@ public partial interface IProgress { void Report(T value); } + public partial interface ISpanFormattable : System.IFormattable + { + bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider); + } public partial class Lazy<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]T> { public Lazy() { } @@ -3394,7 +3401,7 @@ public partial struct RuntimeTypeHandle : System.Runtime.Serialization.ISerializ public static bool operator !=(System.RuntimeTypeHandle left, object? right) { throw null; } } [System.CLSCompliantAttribute(false)] - public readonly partial struct SByte : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct SByte : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly sbyte _dummyPrimitive; public const sbyte MaxValue = (sbyte)127; @@ -3440,7 +3447,7 @@ public sealed partial class SerializableAttribute : System.Attribute { public SerializableAttribute() { } } - public readonly partial struct Single : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct Single : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly float _dummyPrimitive; public const float Epsilon = 1E-45f; @@ -3800,7 +3807,7 @@ public partial class ThreadStaticAttribute : System.Attribute { public ThreadStaticAttribute() { } } - public readonly struct TimeOnly : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + public readonly struct TimeOnly : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { public static System.TimeOnly MinValue { get { throw null; } } public static System.TimeOnly MaxValue { get { throw null; } } @@ -3872,7 +3879,7 @@ public partial class TimeoutException : System.SystemException public TimeoutException(string? message) { } public TimeoutException(string? message, System.Exception? innerException) { } } - public readonly partial struct TimeSpan : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable + public readonly partial struct TimeSpan : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { private readonly int _dummyPrimitive; public static readonly System.TimeSpan MaxValue; @@ -4631,7 +4638,7 @@ public partial class TypeUnloadedException : System.SystemException public TypeUnloadedException(string? message, System.Exception? innerException) { } } [System.CLSCompliantAttribute(false)] - public readonly partial struct UInt16 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct UInt16 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly ushort _dummyPrimitive; public const ushort MaxValue = (ushort)65535; @@ -4673,7 +4680,7 @@ public partial class TypeUnloadedException : System.SystemException public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, out System.UInt16 result) { throw null; } } [System.CLSCompliantAttribute(false)] - public readonly partial struct UInt32 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct UInt32 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly uint _dummyPrimitive; public const uint MaxValue = (uint)4294967295; @@ -4715,7 +4722,7 @@ public partial class TypeUnloadedException : System.SystemException public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, out System.UInt32 result) { throw null; } } [System.CLSCompliantAttribute(false)] - public readonly partial struct UInt64 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IFormattable + public readonly partial struct UInt64 : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.ISpanFormattable { private readonly ulong _dummyPrimitive; public const ulong MaxValue = (ulong)18446744073709551615; @@ -4757,7 +4764,7 @@ public partial class TypeUnloadedException : System.SystemException public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, out System.UInt64 result) { throw null; } } [System.CLSCompliantAttribute(false)] - public readonly partial struct UIntPtr : System.IComparable, System.IComparable, System.IEquatable, System.IFormattable, System.Runtime.Serialization.ISerializable + public readonly partial struct UIntPtr : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable, System.Runtime.Serialization.ISerializable { private readonly int _dummyPrimitive; public static readonly System.UIntPtr Zero; @@ -5200,7 +5207,7 @@ public abstract partial class ValueType public override int GetHashCode() { throw null; } public override string? ToString() { throw null; } } - public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable + public sealed partial class Version : System.ICloneable, System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { public Version() { } public Version(int major, int minor) { } @@ -5233,6 +5240,8 @@ public sealed partial class Version : System.ICloneable, System.IComparable, Sys public bool TryFormat(System.Span destination, out int charsWritten) { throw null; } public static bool TryParse(System.ReadOnlySpan input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version? result) { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Version? result) { throw null; } + bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + string System.IFormattable.ToString(string? format, IFormatProvider? formatProvider) { throw null; } } public partial struct Void { @@ -9584,6 +9593,27 @@ public sealed partial class InternalsVisibleToAttribute : System.Attribute public bool AllInternalsVisible { get { throw null; } set { } } public string AssemblyName { get { throw null; } } } + public ref struct InterpolatedStringBuilder + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public void AppendLiteral(string value) { } + public void AppendFormatted(System.ReadOnlySpan value) { } + public void AppendFormatted(System.ReadOnlySpan value, int alignment = 0, string? format = null) { } + public void AppendFormatted(T value) { } + public void AppendFormatted(T value, string? format) { } + public void AppendFormatted(T value, int alignment) { } + public void AppendFormatted(T value, int alignment, string? format) { } + 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 scratchBuffer) { throw null; } + public static System.Runtime.CompilerServices.InterpolatedStringBuilder Create(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span scratchBuffer) { throw null; } + public override string ToString() { throw null; } + public string ToStringAndClear() { throw null; } + } [System.AttributeUsageAttribute(System.AttributeTargets.Struct)] [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public sealed partial class IsByRefLikeAttribute : System.Attribute @@ -10934,7 +10964,7 @@ public enum NormalizationForm FormKC = 5, FormKD = 6, } - public readonly partial struct Rune : System.IComparable, System.IComparable, System.IEquatable + public readonly partial struct Rune : System.IComparable, System.IComparable, System.IEquatable, System.ISpanFormattable { private readonly int _dummyPrimitive; public Rune(char ch) { throw null; } @@ -11000,6 +11030,8 @@ public enum NormalizationForm public bool TryEncodeToUtf8(System.Span destination, out int bytesWritten) { throw null; } public static bool TryGetRuneAt(string input, int index, out System.Text.Rune value) { throw null; } int System.IComparable.CompareTo(object? obj) { throw null; } + bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + string System.IFormattable.ToString(string? format, IFormatProvider? formatProvider) { throw null; } } public sealed partial class StringBuilder : System.Runtime.Serialization.ISerializable { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index 341cb85b7a35d..4d8c8213f2741 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -220,6 +220,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System/Runtime/CompilerServices/InterpolatedStringBuilderTests.cs b/src/libraries/System.Runtime/tests/System/Runtime/CompilerServices/InterpolatedStringBuilderTests.cs new file mode 100644 index 0000000000000..a0cd121ee7a4d --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Runtime/CompilerServices/InterpolatedStringBuilderTests.cs @@ -0,0 +1,635 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Text; +using Xunit; + +namespace System.Runtime.CompilerServices.Tests +{ + public class InterpolatedStringBuilderTests + { + [Theory] + [InlineData(0, 0)] + [InlineData(1, 1)] + [InlineData(42, 84)] + [InlineData(-1, 0)] + [InlineData(-1, -1)] + [InlineData(-16, 1)] + public void LengthAndHoleArguments_Valid(int literalLength, int formattedCount) + { + InterpolatedStringBuilder.Create(literalLength, formattedCount); + + Span 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); + + InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, default); + InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, scratch1); + InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, Array.Empty()); + InterpolatedStringBuilder.Create(literalLength, formattedCount, provider, new char[256]); + } + + InterpolatedStringBuilder.Create(literalLength, formattedCount, Span.Empty); + InterpolatedStringBuilder.Create(literalLength, formattedCount, scratch1); + InterpolatedStringBuilder.Create(literalLength, formattedCount, Array.Empty()); + InterpolatedStringBuilder.Create(literalLength, formattedCount, new char[256]); + } + + [Fact] + public void ToString_DoesntClear() + { + InterpolatedStringBuilder builder = InterpolatedStringBuilder.Create(0, 0); + builder.AppendLiteral("hi"); + for (int i = 0; i < 3; i++) + { + Assert.Equal("hi", builder.ToString()); + } + Assert.Equal("hi", builder.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()); + } + + [Fact] + public void AppendLiteral() + { + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0); + + foreach (string s in new[] { "", "a", "bc", "def", "this is a long string", "!" }) + { + expected.Append(s); + actual.AppendLiteral(s); + } + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + [Fact] + public void AppendFormatted_ReadOnlySpanChar() + { + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0); + + foreach (string s in new[] { "", "a", "bc", "def", "this is a longer string", "!" }) + { + // span + expected.Append(s); + actual.AppendFormatted((ReadOnlySpan)s); + + // span, format + expected.AppendFormat("{0:X2}", s); + actual.AppendFormatted((ReadOnlySpan)s, format: "X2"); + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // span, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", s); + actual.AppendFormatted((ReadOnlySpan)s, alignment); + + // span, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", s); + actual.AppendFormatted((ReadOnlySpan)s, alignment, "X2"); + } + } + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + [Fact] + public void AppendFormatted_String() + { + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0); + + foreach (string s in new[] { null, "", "a", "bc", "def", "this is a longer string", "!" }) + { + // string + expected.AppendFormat("{0}", s); + actual.AppendFormatted(s); + + // string, format + expected.AppendFormat("{0:X2}", s); + actual.AppendFormatted(s, "X2"); + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // string, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", s); + actual.AppendFormatted(s, alignment); + + // string, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", s); + actual.AppendFormatted(s, alignment, "X2"); + } + } + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + [Fact] + public void AppendFormatted_String_ICustomFormatter() + { + var provider = new ConcatFormatter(); + + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0, provider); + + foreach (string s in new[] { null, "", "a" }) + { + // string + expected.AppendFormat(provider, "{0}", s); + actual.AppendFormatted(s); + + // string, format + expected.AppendFormat(provider, "{0:X2}", s); + actual.AppendFormatted(s, "X2"); + + // string, alignment + expected.AppendFormat(provider, "{0,3}", s); + actual.AppendFormatted(s, 3); + + // string, alignment, format + expected.AppendFormat(provider, "{0,-3:X2}", s); + actual.AppendFormatted(s, -3, "X2"); + } + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + [Fact] + public void AppendFormatted_ReferenceTypes() + { + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0); + + foreach (string rawInput in new[] { null, "", "a", "bc", "def", "this is a longer string", "!" }) + { + foreach (object o in new object[] + { + rawInput, // raw string directly; ToString will return itself + new StringWrapper(rawInput), // wrapper object that returns string from ToString + new FormattableStringWrapper(rawInput), // IFormattable wrapper around string + new SpanFormattableStringWrapper(rawInput) // ISpanFormattable wrapper around string + }) + { + // object + expected.AppendFormat("{0}", o); + actual.AppendFormatted(o); + if (o is IHasToStringState tss1) + { + Assert.True(string.IsNullOrEmpty(tss1.ToStringState.LastFormat)); + AssertModeMatchesType(tss1); + } + + // object, format + expected.AppendFormat("{0:X2}", o); + actual.AppendFormatted(o, "X2"); + if (o is IHasToStringState tss2) + { + Assert.Equal("X2", tss2.ToStringState.LastFormat); + AssertModeMatchesType(tss2); + } + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // object, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", o); + actual.AppendFormatted(o, alignment); + if (o is IHasToStringState tss3) + { + Assert.True(string.IsNullOrEmpty(tss3.ToStringState.LastFormat)); + AssertModeMatchesType(tss3); + } + + // object, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", o); + actual.AppendFormatted(o, alignment, "X2"); + if (o is IHasToStringState tss4) + { + Assert.Equal("X2", tss4.ToStringState.LastFormat); + AssertModeMatchesType(tss4); + } + } + } + } + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + 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); + + foreach (IHasToStringState tss in new IHasToStringState[] { new FormattableStringWrapper("hello"), new SpanFormattableStringWrapper("hello") }) + { + builder.AppendFormatted(tss); + Assert.Same(provider, tss.ToStringState.LastProvider); + + builder.AppendFormatted(tss, 1); + Assert.Same(provider, tss.ToStringState.LastProvider); + + builder.AppendFormatted(tss, "X2"); + Assert.Same(provider, tss.ToStringState.LastProvider); + + builder.AppendFormatted(tss, 1, "X2"); + Assert.Same(provider, tss.ToStringState.LastProvider); + } + } + + [Fact] + public void AppendFormatted_ReferenceTypes_ICustomFormatter() + { + var provider = new ConcatFormatter(); + + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0, provider); + + foreach (string s in new[] { null, "", "a" }) + { + foreach (IHasToStringState tss in new IHasToStringState[] { new FormattableStringWrapper(s), new SpanFormattableStringWrapper(s) }) + { + void AssertTss(IHasToStringState tss, string format) + { + Assert.Equal(format, tss.ToStringState.LastFormat); + Assert.Same(provider, tss.ToStringState.LastProvider); + Assert.Equal(ToStringMode.ICustomFormatterFormat, tss.ToStringState.ToStringMode); + } + + // object + expected.AppendFormat(provider, "{0}", tss); + actual.AppendFormatted(tss); + AssertTss(tss, null); + + // object, format + expected.AppendFormat(provider, "{0:X2}", tss); + actual.AppendFormatted(tss, "X2"); + AssertTss(tss, "X2"); + + // object, alignment + expected.AppendFormat(provider, "{0,3}", tss); + actual.AppendFormatted(tss, 3); + AssertTss(tss, null); + + // object, alignment, format + expected.AppendFormat(provider, "{0,-3:X2}", tss); + actual.AppendFormatted(tss, -3, "X2"); + AssertTss(tss, "X2"); + } + } + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + [Fact] + public void AppendFormatted_ValueTypes() + { + void Test(T t) + { + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0); + + // struct + expected.AppendFormat("{0}", t); + actual.AppendFormatted(t); + Assert.True(string.IsNullOrEmpty(((IHasToStringState)t).ToStringState.LastFormat)); + AssertModeMatchesType(((IHasToStringState)t)); + + // struct, format + expected.AppendFormat("{0:X2}", t); + actual.AppendFormatted(t, "X2"); + Assert.Equal("X2", ((IHasToStringState)t).ToStringState.LastFormat); + AssertModeMatchesType(((IHasToStringState)t)); + + foreach (int alignment in new[] { 0, 3, -3 }) + { + // struct, alignment + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + "}", t); + actual.AppendFormatted(t, alignment); + Assert.True(string.IsNullOrEmpty(((IHasToStringState)t).ToStringState.LastFormat)); + AssertModeMatchesType(((IHasToStringState)t)); + + // struct, alignment, format + expected.AppendFormat("{0," + alignment.ToString(CultureInfo.InvariantCulture) + ":X2}", t); + actual.AppendFormatted(t, alignment, "X2"); + Assert.Equal("X2", ((IHasToStringState)t).ToStringState.LastFormat); + AssertModeMatchesType(((IHasToStringState)t)); + } + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + Test(new FormattableInt32Wrapper(42)); + Test(new SpanFormattableInt32Wrapper(84)); + Test((FormattableInt32Wrapper?)new FormattableInt32Wrapper(42)); + Test((SpanFormattableInt32Wrapper?)new SpanFormattableInt32Wrapper(84)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void AppendFormatted_ValueTypes_CreateProviderFlowed(bool useScratch) + { + void Test(T t) + { + var provider = new CultureInfo("en-US"); + InterpolatedStringBuilder builder = useScratch ? + InterpolatedStringBuilder.Create(1, 2, provider, stackalloc char[16]) : + InterpolatedStringBuilder.Create(1, 2, provider); + + builder.AppendFormatted(t); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + + builder.AppendFormatted(t, 1); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + + builder.AppendFormatted(t, "X2"); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + + builder.AppendFormatted(t, 1, "X2"); + Assert.Same(provider, ((IHasToStringState)t).ToStringState.LastProvider); + } + + Test(new FormattableInt32Wrapper(42)); + Test(new SpanFormattableInt32Wrapper(84)); + Test((FormattableInt32Wrapper?)new FormattableInt32Wrapper(42)); + Test((SpanFormattableInt32Wrapper?)new SpanFormattableInt32Wrapper(84)); + } + + [Fact] + public void AppendFormatted_ValueTypes_ICustomFormatter() + { + var provider = new ConcatFormatter(); + + void Test(T t) + { + void AssertTss(T tss, string format) + { + Assert.Equal(format, ((IHasToStringState)tss).ToStringState.LastFormat); + Assert.Same(provider, ((IHasToStringState)tss).ToStringState.LastProvider); + Assert.Equal(ToStringMode.ICustomFormatterFormat, ((IHasToStringState)tss).ToStringState.ToStringMode); + } + + var expected = new StringBuilder(); + InterpolatedStringBuilder actual = InterpolatedStringBuilder.Create(0, 0, provider); + + // struct + expected.AppendFormat(provider, "{0}", t); + actual.AppendFormatted(t); + AssertTss(t, null); + + // struct, format + expected.AppendFormat(provider, "{0:X2}", t); + actual.AppendFormatted(t, "X2"); + AssertTss(t, "X2"); + + // struct, alignment + expected.AppendFormat(provider, "{0,3}", t); + actual.AppendFormatted(t, 3); + AssertTss(t, null); + + // struct, alignment, format + expected.AppendFormat(provider, "{0,-3:X2}", t); + actual.AppendFormatted(t, -3, "X2"); + AssertTss(t, "X2"); + + Assert.Equal(expected.ToString(), actual.ToStringAndClear()); + } + + Test(new FormattableInt32Wrapper(42)); + Test(new SpanFormattableInt32Wrapper(84)); + Test((FormattableInt32Wrapper?)new FormattableInt32Wrapper(42)); + Test((SpanFormattableInt32Wrapper?)new SpanFormattableInt32Wrapper(84)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + 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); + + for (int i = 0; i < 1000; i++) + { + builder.AppendFormatted(i); + expected.Append(i); + + builder.AppendFormatted(i, 3); + expected.AppendFormat("{0,3}", i); + } + + Assert.Equal(expected.ToString(), builder.ToStringAndClear()); + } + + private static void AssertModeMatchesType(T tss) where T : IHasToStringState + { + ToStringMode expected = + tss is ISpanFormattable ? ToStringMode.ISpanFormattableTryFormat : + tss is IFormattable ? ToStringMode.IFormattableToString : + ToStringMode.ObjectToString; + Assert.Equal(expected, tss.ToStringState.ToStringMode); + } + + private sealed class SpanFormattableStringWrapper : ISpanFormattable, IHasToStringState + { + private readonly string _value; + public ToStringState ToStringState { get; } = new ToStringState(); + + public SpanFormattableStringWrapper(string value) => _value = value; + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + ToStringState.LastFormat = format.ToString(); + ToStringState.LastProvider = provider; + ToStringState.ToStringMode = ToStringMode.ISpanFormattableTryFormat; + + if (_value is null) + { + charsWritten = 0; + return true; + } + + if (_value.Length > destination.Length) + { + charsWritten = 0; + return false; + } + + charsWritten = _value.Length; + _value.AsSpan().CopyTo(destination); + return true; + } + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value; + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value; + } + } + + private struct SpanFormattableInt32Wrapper : ISpanFormattable, IHasToStringState + { + private readonly int _value; + public ToStringState ToStringState { get; } + + public SpanFormattableInt32Wrapper(int value) + { + ToStringState = new ToStringState(); + _value = value; + } + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider provider) + { + ToStringState.LastFormat = format.ToString(); + ToStringState.LastProvider = provider; + ToStringState.ToStringMode = ToStringMode.ISpanFormattableTryFormat; + + return _value.TryFormat(destination, out charsWritten, format, provider); + } + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value.ToString(format, formatProvider); + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value.ToString(); + } + } + + private sealed class FormattableStringWrapper : IFormattable, IHasToStringState + { + private readonly string _value; + public ToStringState ToStringState { get; } = new ToStringState(); + + public FormattableStringWrapper(string s) => _value = s; + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value; + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value; + } + } + + private struct FormattableInt32Wrapper : IFormattable, IHasToStringState + { + private readonly int _value; + public ToStringState ToStringState { get; } + + public FormattableInt32Wrapper(int i) + { + ToStringState = new ToStringState(); + _value = i; + } + + public string ToString(string format, IFormatProvider formatProvider) + { + ToStringState.LastFormat = format; + ToStringState.LastProvider = formatProvider; + ToStringState.ToStringMode = ToStringMode.IFormattableToString; + return _value.ToString(format, formatProvider); + } + + public override string ToString() + { + ToStringState.LastFormat = null; + ToStringState.LastProvider = null; + ToStringState.ToStringMode = ToStringMode.ObjectToString; + return _value.ToString(); + } + } + + private sealed class ToStringState + { + public string LastFormat { get; set; } + public IFormatProvider LastProvider { get; set; } + public ToStringMode ToStringMode { get; set; } + } + + private interface IHasToStringState + { + ToStringState ToStringState { get; } + } + + private enum ToStringMode + { + ObjectToString, + IFormattableToString, + ISpanFormattableTryFormat, + ICustomFormatterFormat, + } + + private sealed class StringWrapper + { + private readonly string _value; + + public StringWrapper(string s) => _value = s; + + public override string ToString() => _value; + } + + private sealed class ConcatFormatter : IFormatProvider, ICustomFormatter + { + public object GetFormat(Type formatType) => formatType == typeof(ICustomFormatter) ? this : null; + + public string Format(string format, object arg, IFormatProvider formatProvider) + { + string s = format + " " + arg + formatProvider; + + if (arg is IHasToStringState tss) + { + // Set after using arg.ToString() in concat above + tss.ToStringState.LastFormat = format; + tss.ToStringState.LastProvider = formatProvider; + tss.ToStringState.ToStringMode = ToStringMode.ICustomFormatterFormat; + } + + return s; + } + } + } +}