using System.Buffers;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
namespace System.Runtime.CompilerServices;
/// Provides a handler used by the language compiler to process interpolated strings into instances.
[InterpolatedStringHandler]
public ref struct DefaultInterpolatedStringHandler
{
// Implementation note:
// As this type lives in CompilerServices and is only intended to be targeted by the compiler,
// public APIs eschew argument validation logic in a variety of places, e.g. allowing a null input
// when one isn't expected to produce a NullReferenceException rather than an ArgumentNullException.
/// Expected average length of formatted data used for an individual interpolation expression result.
///
/// 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 format items themselves, e.g. "{0}", and since it's rare to have double-digit
/// numbers of items, we bump the 8 up to 11 to account for the three extra characters in "{d}",
/// since the compiler-provided base length won't include the equivalent character count.
///
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;
/// 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;
/// Whether provides an ICustomFormatter.
///
/// Custom formatters are very rare. We want to support them, but it's ok if we make them more expensive
/// in order to make them as pay-for-play as possible. So, we avoid adding another reference type field
/// to reduce the size of the handler and to reduce required zero'ing, by only storing whether the provider
/// provides a formatter, rather than actually storing the formatter. This in turn means, if there is a
/// formatter, we pay for the extra interface call on each AppendFormatted that needs it.
///
private readonly bool _hasCustomFormatter;
/// Creates a handler used to translate an interpolated string into a .
/// The number of constant characters outside of interpolation expressions in the interpolated string.
/// The number of interpolation expressions 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 DefaultInterpolatedStringHandler(int literalLength, int formattedCount)
{
this._provider = null;
this._chars = this._arrayToReturnToPool = ArrayPool.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
this._pos = 0;
this._hasCustomFormatter = false;
}
/// Creates a handler used to translate an interpolated string into a .
/// The number of constant characters outside of interpolation expressions in the interpolated string.
/// The number of interpolation expressions 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 DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider)
{
this._provider = provider;
this._chars = this._arrayToReturnToPool = ArrayPool.Shared.Rent(GetDefaultLength(literalLength, formattedCount));
this._pos = 0;
this._hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
}
/// Creates a handler used to translate an interpolated string into a .
/// The number of constant characters outside of interpolation expressions in the interpolated string.
/// The number of interpolation expressions in the interpolated string.
/// An object that supplies culture-specific formatting information.
/// A buffer temporarily transferred to the handler 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 DefaultInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider? provider, Span initialBuffer)
{
this._provider = provider;
this._chars = initialBuffer;
this._arrayToReturnToPool = null;
this._pos = 0;
this._hasCustomFormatter = provider is not null && HasCustomFormatter(provider);
}
/// Derives a default length with which to seed the handler.
/// The number of constant characters outside of interpolation expressions in the interpolated string.
/// The number of interpolation expressions in the interpolated string.
[MethodImpl(MethodImplOptions.AggressiveInlining)] // becomes a constant when inputs are constant
internal static int GetDefaultLength(int literalLength, int formattedCount) =>
Math.Max(MinimumArrayPoolLength, literalLength + (formattedCount * GuessedLengthPerHole));
/// Gets the built .
/// The built string.
public override string ToString() => new string(this.Text);
/// Gets the built and clears the handler.
/// The built string.
///
/// This releases any resources used by the handler. The method should be invoked only
/// once and as the last thing performed on the handler. Subsequent use is erroneous, ill-defined,
/// and may destabilize the process, as may using any other copies of the handler after ToStringAndClear
/// is called on any one of them.
///
public string ToStringAndClear()
{
string result = new string(this.Text);
this.Clear();
return result;
}
/// Clears the handler, returning any rented array to the pool.
[MethodImpl(MethodImplOptions.AggressiveInlining)] // used only on a few hot paths
internal void Clear()
{
char[]? toReturn = this._arrayToReturnToPool;
this = default; // defensive clear
if (toReturn is not null)
{
ArrayPool.Shared.Return(toReturn);
}
}
/// Gets a span of the written characters thus far.
internal ReadOnlySpan Text => this._chars.Slice(0, this._pos);
/// Writes the specified string to the handler.
/// The string to write.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendLiteral(string value)
{
// AppendLiteral is expected to always be called by compiler-generated code with a literal string.
// By inlining it, the method body is exposed to the constant length of that literal, allowing the JIT to
// prune away the irrelevant cases. This effectively enables multiple implementations of AppendLiteral,
// special-cased on and optimized for the literal's length. We special-case lengths 1 and 2 because
// they're very common, e.g.
// 1: ' ', '.', '-', '\t', etc.
// 2: ", ", "0x", "=>", ": ", etc.
// but we refrain from adding more because, in the rare case where AppendLiteral is called with a non-literal,
// there is a lot of code here to be inlined.
// TODO: https://github.com/dotnet/runtime/issues/41692#issuecomment-685192193
// What we really want here is to be able to add a bunch of additional special-cases based on length,
// e.g. a switch with a case for each length <= 8, not mark the method as AggressiveInlining, and have
// it inlined when provided with a string literal such that all the other cases evaporate but not inlined
// if called directly with something that doesn't enable pruning. Even better, if "literal".TryCopyTo
// could be unrolled based on the literal, ala https://github.com/dotnet/runtime/pull/46392, we might
// be able to remove all special-casing here.
if (value.Length == 1)
{
Span chars = this._chars;
int pos = this._pos;
if ((uint)pos < (uint)chars.Length)
{
chars[pos] = value[0];
this._pos = pos + 1;
}
else
{
this.GrowThenCopyString(value);
}
return;
}
if (value.Length == 2)
{
Span chars = this._chars;
int pos = this._pos;
if ((uint)pos < chars.Length - 1)
{
ref var valueRef =ref MemoryMarshal.GetReference(value.AsSpan());
Unsafe.WriteUnaligned(
ref Unsafe.As(ref Unsafe.Add(ref MemoryMarshal.GetReference(chars), pos)),
Unsafe.ReadUnaligned(ref Unsafe.As(ref valueRef)));
this._pos = pos + 2;
}
else
{
this.GrowThenCopyString(value);
}
return;
}
this.AppendStringDirect(value);
}
/// Writes the specified string to the handler.
/// The string to write.
private void AppendStringDirect(string value)
{
if (value.AsSpan().TryCopyTo(this._chars.Slice(this._pos)))
{
this._pos += value.Length;
}
else
{
this.GrowThenCopyString(value);
}
}
#region AppendFormatted
// Design note:
// The compiler requires a AppendFormatted overload for anything that might be within an interpolation expression;
// if it can't find an appropriate overload, for handlers in general it'll simply fail to compile.
// (For target-typing to string where it uses DefaultInterpolatedStringHandler implicitly, it'll instead fall back to
// its other mechanisms, e.g. using string.Format. This fallback has the benefit that if we miss a case,
// interpolated strings will still work, but it has the downside that a developer generally won't know
// if the fallback is happening and they're paying more.)
//
// 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 an interpolation expression would be boxed.
// - ReadOnlySpan could not be used in interpolation expressions.
// - Every AppendFormatted call would have three arguments at the call site, bloating the IL further.
// - Every invocation would be more expensive, due to lack of specialization, every call needing to account
// for alignment and format, etc.
//
// 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 handler is used in places where the fallback isn't provided), because the compiler
// can't always target type to T, e.g. `b switch { true => 1, false => null }` where `b` is a bool can successfully
// be passed as an argument of type `object` but not of type `T`.
// - Reference types get no benefit from going through the generic code paths, and actually incur some overheads
// 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 DefaultInterpolatedStringHandler, since it is the most common handler
// and we want to minimize overheads both at runtime and in IL size, e.g. have a complete set of overloads for each of:
// (T, ...) where T : struct
// (T?, ...) where T : struct
// (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 handlers 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 handler.
/// 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 (this._hasCustomFormatter)
{
this.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 formattable)
{
// 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 = formattable.ToString(format: null, this._provider); // constrained call avoiding boxing for value types
}
else
{
s = value?.ToString();
}
if (s is not null)
{
this.AppendStringDirect(s);
}
}
/// Writes the specified value to the handler.
/// The value to write.
/// The format string.
public void AppendFormatted(T value, string? format)
{
// If there's a custom formatter, always use it.
if (this._hasCustomFormatter)
{
this.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 formattable)
{
// 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 = formattable.ToString(format, this._provider); // constrained call avoiding boxing for value types
}
else
{
s = value?.ToString();
}
if (s is not null)
{
this.AppendStringDirect(s);
}
}
/// Writes the specified value to the handler.
/// 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 = this._pos;
this.AppendFormatted(value);
if (alignment != 0)
{
this.AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
}
}
/// Writes the specified value to the handler.
/// 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 = this._pos;
this.AppendFormatted(value, format);
if (alignment != 0)
{
this.AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
}
}
#endregion
#region AppendFormatted ReadOnlySpan
/// Writes the specified character span to the handler.
/// The span to write.
public void AppendFormatted(ReadOnlySpan value)
{
// Fast path for when the value fits in the current buffer
if (value.TryCopyTo(this._chars.Slice(this._pos)))
{
this._pos += value.Length;
}
else
{
this.GrowThenCopySpan(value);
}
}
/// Writes the specified string of chars to the handler.
/// 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.
this.AppendFormatted(value);
return;
}
// Write the value along with the appropriate padding.
this.EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
if (leftAlign)
{
value.CopyTo(this._chars.Slice(this._pos));
this._pos += value.Length;
this._chars.Slice(this._pos, paddingRequired).Fill(' ');
this._pos += paddingRequired;
}
else
{
this._chars.Slice(this._pos, paddingRequired).Fill(' ');
this._pos += paddingRequired;
value.CopyTo(this._chars.Slice(this._pos));
this._pos += value.Length;
}
}
#endregion
#region AppendFormatted string
/// Writes the specified value to the handler.
/// 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 (!this._hasCustomFormatter &&
value is not null &&
value.AsSpan().TryCopyTo(this._chars.Slice(this._pos)))
{
this._pos += value.Length;
}
else
{
this.AppendFormattedSlow(value);
}
}
/// Writes the specified value to the handler.
/// 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 (this._hasCustomFormatter)
{
this.AppendCustomFormatter(value, format: null);
}
else if (value is not null)
{
this.EnsureCapacityForAdditionalChars(value.Length);
value.AsSpan().CopyTo(this._chars.Slice(this._pos));
this._pos += value.Length;
}
}
/// Writes the specified value to the handler.
/// 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.
this.AppendFormatted(value, alignment, format);
#endregion
#region AppendFormatted object
/// Writes the specified value to the handler.
/// 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.
this.AppendFormatted