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