File: Utilities\ValueStringBuilder.cs
Web Access
Project: ..\..\..\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
// Adapted from JeremyKuhne/touki:touki/Touki/Text/ValueStringBuilder.cs
//   https://github.com/JeremyKuhne/touki/blob/main/touki/Touki/Text/ValueStringBuilder.cs
// Original source: .NET Runtime and Windows Forms source code (MIT-licensed).
//
// Adaptations for MSBuild:
//   - Moved into the Microsoft.Build.Utilities namespace.
//   - Removed all [InterpolatedStringHandler]-related members
//     (the handler attribute, the (literalLength, formattedCount) constructors,
//     the format-provider fields, AppendLiteral, AppendFormatted, etc.).
//   - Removed Touki-specific dependencies (StringSegment, CopyTo overloads
//     that use .NET-only TextWriter / StringBuilder APIs).
//   - Polyfilled Span<char>.Replace for net472 with a manual loop.
 
using System;
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
 
namespace Microsoft.Build.Utilities;
 
/// <summary>
///  String builder struct that can be used to build strings in a memory-efficient way.
///  Allows using stack space for small strings.
/// </summary>
internal ref struct ValueStringBuilder
{
    // We're using a byte array here so the same backing buffer can be passed to APIs that take byte[] without
    // type-safety juggling. Byte arrays are always allocated on pointer-size boundaries so they're safe for
    // reinterpreting as Span<char>.
    private byte[]? _arrayToReturnToPool;
    private Span<char> _chars;
    private int _length;
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="ValueStringBuilder"/> struct with an initial buffer.
    /// </summary>
    /// <param name="initialBuffer">The initial buffer to use for the string builder.</param>
    public ValueStringBuilder(Span<char> initialBuffer)
    {
        _arrayToReturnToPool = null;
        _chars = initialBuffer;
        _length = 0;
    }
 
    /// <summary>
    ///  Initializes a new instance of the <see cref="ValueStringBuilder"/> struct with the specified initial capacity.
    /// </summary>
    /// <param name="initialCapacity">The initial capacity for the string builder.</param>
    public ValueStringBuilder(int initialCapacity)
    {
        _arrayToReturnToPool = ArrayPool<byte>.Shared.Rent(initialCapacity * sizeof(char));
        _chars = MemoryMarshal.Cast<byte, char>((Span<byte>)_arrayToReturnToPool);
        _length = 0;
    }
 
    /// <summary>
    ///  Gets or sets the current length of the string builder.
    /// </summary>
    public int Length
    {
        readonly get => _length;
        set
        {
            // Do not free if set to 0. This allows in-place building of strings.
            ArgumentOutOfRangeException.ThrowIfNegative(value);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(value, _chars.Length);
            _length = value;
        }
    }
 
    /// <summary>
    ///  Gets the maximum number of characters that can be contained in the memory allocated by the current instance.
    /// </summary>
    public readonly int Capacity => _chars.Length;
 
    /// <summary>
    ///  Clears the contents of the string builder, resetting its length to zero.
    /// </summary>
    public void Clear()
    {
        // Reset the position to 0, but do not clear the underlying array to allow in-place building.
        _length = 0;
    }
 
    /// <summary>
    ///  Ensures that the capacity of this builder is at least the specified value.
    /// </summary>
    public void EnsureCapacity(int capacity)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(capacity);
 
        if ((uint)capacity > (uint)_chars.Length)
        {
            Grow(capacity - _length);
        }
    }
 
    /// <summary>
    ///  Get a pinnable reference to the builder. Always ensures that there is a trailing null.
    /// </summary>
    /// <remarks>
    ///  This overload is pattern matched in the C# 7.3+ compiler so you can omit
    ///  the explicit method call, and write e.g. <c>fixed (char* c = builder)</c>.
    /// </remarks>
    public ref char GetPinnableReference()
    {
        EnsureCapacity(_length + 1);
        _chars[_length] = '\0';
        return ref _chars.GetPinnableReference();
    }
 
    /// <summary>
    ///  Gets a reference to the character at the specified index.
    /// </summary>
    public ref char this[int index]
    {
        get
        {
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(index, _length);
            return ref _chars[index];
        }
    }
 
    /// <summary>
    ///  Converts the value of this builder to a <see cref="string"/>.
    /// </summary>
    public override readonly string ToString() => _chars[.._length].ToString();
 
    /// <summary>
    ///  Converts the value of this builder to a <see cref="string"/> and disposes the builder.
    /// </summary>
    public string ToStringAndDispose()
    {
        string s = ToString();
        Dispose();
        return s;
    }
 
    /// <summary>
    ///  Returns a span around the contents of the builder.
    /// </summary>
    /// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/>.</param>
    public ReadOnlySpan<char> AsSpan(bool terminate)
    {
        if (terminate)
        {
            EnsureCapacity(Length + 1);
            _chars[Length] = '\0';
        }
 
        return _chars[.._length];
    }
 
    /// <summary>
    ///  Returns a read-only span around the contents of the builder.
    /// </summary>
    public readonly ReadOnlySpan<char> AsSpan() => _chars[.._length];
 
    /// <summary>
    ///  Gets a slice of the builder using the specified range.
    /// </summary>
    public readonly ReadOnlySpan<char> this[Range range] => _chars[.._length][range];
 
    /// <summary>
    ///  Forms a slice out of the buffer starting at a specified index.
    /// </summary>
    public readonly ReadOnlySpan<char> Slice(int start)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(start);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(start, _length);
        return _chars[start.._length];
    }
 
    /// <summary>
    ///  Forms a slice out of the buffer starting at a specified index for a specified length.
    /// </summary>
    public readonly ReadOnlySpan<char> Slice(int start, int length)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(start);
        ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(start, _length);
        ArgumentOutOfRangeException.ThrowIfNegative(length);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(start + length, _length);
 
        return _chars.Slice(start, length);
    }
 
    /// <summary>
    ///  Returns a span of the requested length at the current position in the builder that can be
    ///  used to directly append characters.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Span<char> AppendSpan(int length)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(length);
 
        int originalLength = _length;
        if (originalLength > _chars.Length - length)
        {
            Grow(length);
        }
 
        _length = originalLength + length;
        return _chars.Slice(originalLength, length);
    }
 
    /// <summary>
    ///  Attempts to copy the contents of this builder to the destination span.
    /// </summary>
    public readonly bool TryCopyTo(Span<char> destination, out int charsWritten)
    {
        if (_chars[.._length].TryCopyTo(destination))
        {
            charsWritten = _length;
            return true;
        }
 
        charsWritten = 0;
        return false;
    }
 
    /// <summary>
    ///  Inserts the specified character a specified number of times at the specified index.
    /// </summary>
    public void Insert(int index, char value, int count)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(index, _length);
        ArgumentOutOfRangeException.ThrowIfNegative(count);
 
        if (_length > _chars.Length - count)
        {
            Grow(count);
        }
 
        int remaining = _length - index;
        _chars.Slice(index, remaining).CopyTo(_chars[(index + count)..]);
        _chars.Slice(index, count).Fill(value);
        _length += count;
    }
 
    /// <summary>
    ///  Inserts the specified string at the specified index.
    /// </summary>
    public void Insert(int index, string? s)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(index);
        ArgumentOutOfRangeException.ThrowIfGreaterThan(index, _length);
 
        if (s is null)
        {
            return;
        }
 
        int count = s.Length;
 
        if (_length > _chars.Length - count)
        {
            Grow(count);
        }
 
        int remaining = _length - index;
        _chars.Slice(index, remaining).CopyTo(_chars[(index + count)..]);
        s.AsSpan().CopyTo(_chars[index..]);
        _length += count;
    }
 
    /// <summary>
    ///  Appends the specified character to the end of this builder.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Append(char c)
    {
        int pos = _length;
        if ((uint)pos < (uint)_chars.Length)
        {
            _chars[pos] = c;
            _length = pos + 1;
        }
        else
        {
            GrowAndAppend(c);
        }
    }
 
    /// <summary>
    ///  Appends a string to the builder. If the string is <see langword="null"/>, this method does nothing.
    /// </summary>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Append(string? s)
    {
        if (s is null)
        {
            return;
        }
 
        int pos = _length;
        if (s.Length == 1 && (uint)pos < (uint)_chars.Length)
        {
            // Very common case, e.g. appending separators or single-char delimiters.
            _chars[pos] = s[0];
            _length = pos + 1;
        }
        else
        {
            AppendSlow(s);
        }
    }
 
    private void AppendSlow(string s)
    {
        int pos = _length;
        if (pos > _chars.Length - s.Length)
        {
            Grow(s.Length);
        }
 
        s.AsSpan().CopyTo(_chars[pos..]);
        _length += s.Length;
    }
 
    /// <summary>
    ///  Appends the specified character a specified number of times to the end of this builder.
    /// </summary>
    public void Append(char c, int count)
    {
        if (_length > _chars.Length - count)
        {
            Grow(count);
        }
 
        Span<char> dst = _chars.Slice(_length, count);
        for (int i = 0; i < dst.Length; i++)
        {
            dst[i] = c;
        }
 
        _length += count;
    }
 
    /// <summary>
    ///  Appends characters from a pointer to the end of this builder.
    /// </summary>
    public unsafe void Append(char* value, int length)
    {
        int pos = _length;
        if (pos > _chars.Length - length)
        {
            Grow(length);
        }
 
        Span<char> dst = _chars.Slice(_length, length);
        for (int i = 0; i < dst.Length; i++)
        {
            dst[i] = *value++;
        }
 
        _length += length;
    }
 
    /// <summary>
    ///  Appends the specified read-only span of characters to the end of this builder.
    /// </summary>
    public void Append(scoped ReadOnlySpan<char> value)
    {
        int pos = _length;
        if (pos > _chars.Length - value.Length)
        {
            Grow(value.Length);
        }
 
        value.CopyTo(_chars[_length..]);
        _length += value.Length;
    }
 
    /// <summary>
    ///  Replace all occurrences of a character in the builder with another character.
    /// </summary>
    public readonly void Replace(char oldValue, char newValue)
    {
        if (_length == 0 || oldValue == newValue)
        {
            return;
        }
 
        _chars[.._length].Replace(oldValue, newValue);
    }
 
    [MethodImpl(MethodImplOptions.NoInlining)]
    private void GrowAndAppend(char c)
    {
        Grow(1);
        Append(c);
    }
 
    /// <summary>
    ///  Resize the internal buffer either by doubling current buffer size or by adding
    ///  <paramref name="additionalCapacityBeyondPos"/> to <see cref="_length"/> whichever is greater.
    /// </summary>
    [MethodImpl(MethodImplOptions.NoInlining)]
    [MemberNotNull(nameof(_arrayToReturnToPool))]
    private void Grow(int additionalCapacityBeyondPos)
    {
        Debug.Assert(additionalCapacityBeyondPos > 0);
        Debug.Assert(_length > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");
 
        const uint ArrayMaxLength = 0x7FFFFFC7; // same as Array.MaxLength
 
        // Increase to at least the required size, but try to double the size if possible,
        // bounding the doubling to not go beyond the max array length.
        int newCapacity = (int)Math.Max(
            (uint)(_length + additionalCapacityBeyondPos),
            Math.Min((uint)_chars.Length * 2, ArrayMaxLength));
 
        // Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative.
        byte[] poolArray = ArrayPool<byte>.Shared.Rent(newCapacity * sizeof(char));
 
        _chars[.._length].CopyTo(MemoryMarshal.Cast<byte, char>((Span<byte>)poolArray));
 
        byte[]? toReturn = _arrayToReturnToPool;
        _chars = MemoryMarshal.Cast<byte, char>((Span<byte>)poolArray);
        _arrayToReturnToPool = poolArray;
        if (toReturn is not null)
        {
            ArrayPool<byte>.Shared.Return(toReturn);
        }
    }
 
    /// <summary>
    ///  Releases all resources used by the <see cref="ValueStringBuilder"/>.
    /// </summary>
    /// <remarks>
    ///  Returns any rented array back to the array pool and resets the builder to its default state.
    ///  After calling this method the builder should not be used again.
    /// </remarks>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Dispose()
    {
        byte[]? toReturn = _arrayToReturnToPool;
 
        // Clear the fields to prevent accidental reuse.
        _arrayToReturnToPool = null;
        _chars = default;
        _length = 0;
 
        if (toReturn is not null)
        {
            ArrayPool<byte>.Shared.Return(toReturn);
        }
    }
 
    /// <summary>
    ///  Implicitly converts a <see cref="ValueStringBuilder"/> to a <see cref="ReadOnlySpan{T}"/>.
    /// </summary>
    public static implicit operator ReadOnlySpan<char>(ValueStringBuilder builder) => builder._chars[..builder._length];
}