File: Resources\SpanReader.cs
Web Access
Project: Microsoft.DotNet.Cli.CoreUtils.csproj (Microsoft.DotNet.Cli.CoreUtils)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Cli.Resources.Internal;

/// <summary>
///  Fast stack based <see cref="ReadOnlySpan{T}"/> reader.
/// </summary>
/// <remarks>
///  <para>
///   Care must be used when reading struct values that depend on a specific field state for members to work
///   correctly. For example, <see cref="DateTime"/> has a very specific set of valid values for its packed
///   <see langword="ulong"/> field.
///  </para>
///  <para>
///   Inspired by SequenceReader{T} patterns.
///  </para>
/// </remarks>
/// <param name="span">The span to read.</param>
internal unsafe ref struct SpanReader<T>(ReadOnlySpan<T> span) where T : unmanaged, IEquatable<T>
{
    // Deliberately not an auto property for performance.
    private ReadOnlySpan<T> _unread = span;

    /// <summary>
    ///  Gets the original span that the reader was created with.
    /// </summary>
    public ReadOnlySpan<T> Span { get; } = span;

    /// <summary>
    ///  The portion of the original span that has not yet been read.
    /// </summary>
    public readonly ReadOnlySpan<T> Unread => _unread;

    /// <summary>
    ///  Returns <see langword="true"/> if there is no unread data left in the span.
    /// </summary>
    public readonly bool End => _unread.IsEmpty;

    /// <summary>
    ///  Gets or sets the current position of the reader within the span.
    /// </summary>
    /// <value>The zero-based position of the reader. Setting this value repositions the reader.</value>
    /// <exception cref="ArgumentOutOfRangeException">
    ///  Thrown when the value is negative or greater than <see cref="Length"/>.
    /// </exception>
    public int Position
    {
        readonly get => Span.Length - _unread.Length;
        set => _unread = Span[value..];
    }

    /// <summary>
    ///  Gets the total length of the original span.
    /// </summary>
    public readonly int Length => Span.Length;

    /// <summary>
    ///  Splits out the next span of data up to the given <paramref name="delimiter"/> or the end of the unread data.
    ///  Positions the reader to the next unread data after the delimiter.
    /// </summary>
    /// <param name="delimiter">The delimiter to look for.</param>
    /// <param name="span">The read data, if any.</param>
    /// <returns><see langword="true"/> if a segment was found; otherwise, <see langword="false"/>.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TrySplit(T delimiter, out ReadOnlySpan<T> span)
    {
        bool result = false;

        if (_unread.IsEmpty)
        {
            span = default;
        }
        else
        {
            if (!TryReadTo(delimiter, advancePastDelimiter: true, out span))
            {
                span = _unread;
                _unread = default;
            }

            result = true;
        }

        return result;
    }

    /// <summary>
    ///  Splits out the next span of data up to the given <paramref name="delimiters"/> or the end of the unread data.
    ///  Positions the reader to the next unread data after the delimiter.
    /// </summary>
    /// <param name="delimiters">The delimiter to look for.</param>
    /// <param name="span">The read data, if any.</param>
    /// <returns><see langword="true"/> if a segment was found; otherwise, <see langword="false"/>.</returns>
    public bool TrySplitAny(scoped ReadOnlySpan<T> delimiters, out ReadOnlySpan<T> span)
    {
        bool result = false;
        if (_unread.IsEmpty)
        {
            span = default;
        }
        else
        {
            if (!TryReadToAny(delimiters, advancePastDelimiter: true, out span))
            {
                // No delimiter found (or empty delimiters), return all remaining data
                span = _unread;
                _unread = default;
            }

            result = true;
        }

        return result;
    }

    /// <summary>
    ///  Try to read everything up to the given <paramref name="delimiter"/>. Advances the reader past the
    ///  <paramref name="delimiter"/> if found.
    /// </summary>
    /// <inheritdoc cref="TryReadTo(T, bool, out ReadOnlySpan{T})"/>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryReadTo(T delimiter, out ReadOnlySpan<T> span) =>
        TryReadTo(delimiter, advancePastDelimiter: true, out span);

    /// <summary>
    ///  Try to read everything up to the given <paramref name="delimiter"/>.
    /// </summary>
    /// <param name="span">The read data, if any.</param>
    /// <param name="delimiter">The delimiter to look for.</param>
    /// <param name="advancePastDelimiter">
    ///  <see langword="true"/> to move past the <paramref name="delimiter"/> if found.
    /// </param>
    /// <returns><see langword="true"/> if the <paramref name="delimiter"/> was found.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryReadTo(T delimiter, bool advancePastDelimiter, out ReadOnlySpan<T> span)
    {
        bool found = false;
        int index = _unread.IndexOf(delimiter);
        span = default;

        if (index != -1)
        {
            found = true;
            if (index > 0)
            {
                span = _unread;
                UncheckedSliceTo(ref span, index);
            }

            if (advancePastDelimiter)
            {
                index++;
            }

            UnsafeAdvance(index);
        }

        return found;
    }

    /// <summary>
    ///  Try to read everything up to the given <paramref name="delimiters"/>.
    /// </summary>
    /// <param name="span">The read data, if any.</param>
    /// <param name="delimiters">The delimiters to look for.</param>
    /// <param name="advancePastDelimiter">
    ///  True to move past the first found instance of any of the given <paramref name="delimiters"/>.
    /// </param>
    /// <returns>True if any of the <paramref name="delimiters"/> were found.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryReadToAny(scoped ReadOnlySpan<T> delimiters, bool advancePastDelimiter, out ReadOnlySpan<T> span)
    {
        bool found = false;
        span = default;

        int index = delimiters.Length switch
        {
            // The downlevel extension of IndexOfAny returns 0 for empty delimiters.
#if NETFRAMEWORK
            0 => -1,
#endif
            2 => _unread.IndexOfAny(delimiters[0], delimiters[1]),
            _ => _unread.IndexOfAny(delimiters)
        };

        if (index != -1)
        {
            found = true;
            if (index > 0)
            {
                span = _unread;
                UncheckedSliceTo(ref span, index);
            }

            if (advancePastDelimiter)
            {
                index++;
            }

            UnsafeAdvance(index);
        }

        return found;
    }

    /// <summary>
    ///  Try to read the next value.
    /// </summary>
    /// <param name="value">
    ///  When this method returns, contains the value that was read, or the default value if no value could be read.
    /// </param>
    /// <returns><see langword="true"/> if a value was successfully read; otherwise, <see langword="false"/>.</returns>
    public bool TryRead(out T value)
    {
        bool success;

        if (_unread.IsEmpty)
        {
            value = default;
            success = false;
        }
        else
        {
            success = true;
            value = _unread[0];
            UnsafeAdvance(1);
        }

        return success;
    }

    /// <summary>
    ///  Try to read a span of the given <paramref name="count"/>.
    /// </summary>
    /// <param name="count">The number of elements to read.</param>
    /// <param name="span">
    ///  When this method returns, contains the span of data that was read, or an empty span if not enough data was available.
    /// </param>
    /// <returns>
    ///  <see langword="true"/> if the requested number of elements was successfully read; otherwise, <see langword="false"/>.
    /// </returns>
    /// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="count"/> is negative.</exception>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryRead(int count, out ReadOnlySpan<T> span)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(count);

        bool success;

        if (count > _unread.Length)
        {
            span = default;
            success = false;
        }
        else
        {
            success = true;
            span = _unread[..count];
            UnsafeAdvance(count);
        }

        return success;
    }

    /// <summary>
    ///  Try to read a value of the given type. The size of the value must be evenly divisible by the size of
    ///  <typeparamref name="T"/>.
    /// </summary>
    /// <typeparam name="TValue">The type of value to read. Must be an unmanaged type.</typeparam>
    /// <param name="value">
    ///  When this method returns, contains the value that was read, or the default value if not enough data was available.
    /// </param>
    /// <returns>
    ///  <see langword="true"/> if the value was successfully read; otherwise, <see langword="false"/>.
    /// </returns>
    /// <exception cref="ArgumentException">
    ///  Thrown when the size of <typeparamref name="TValue"/> is not evenly divisible by the size of <typeparamref name="T"/>.
    /// </exception>
    /// <remarks>
    ///  <para>
    ///   This is just a straight copy of bits. If <typeparamref name="TValue"/> has methods that depend on
    ///   specific field value constraints this could be unsafe.
    ///  </para>
    ///  <para>
    ///   The compiler will often optimize away the struct copy if you only read from the value.
    ///  </para>
    /// </remarks>
    public bool TryRead<TValue>(out TValue value) where TValue : unmanaged
    {
        if (sizeof(TValue) < sizeof(T) || sizeof(TValue) % sizeof(T) != 0)
        {
            throw new ArgumentException($"The size of {nameof(TValue)} must be evenly divisible by the size of {nameof(T)}.");
        }

        bool success;

        if (sizeof(TValue) > _unread.Length * sizeof(T))
        {
            value = default;
            success = false;
        }
        else
        {
            success = true;
            value = Unsafe.ReadUnaligned<TValue>(ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(_unread)));
            UnsafeAdvance(sizeof(TValue) / sizeof(T));
        }

        return success;
    }

    /// <summary>
    ///  Try to read a span of values of the given type. The size of the value must be evenly divisible by the size of
    ///  <typeparamref name="T"/>.
    /// </summary>
    /// <typeparam name="TValue">The type of values to read. Must be an unmanaged type.</typeparam>
    /// <param name="count">The number of values to read.</param>
    /// <param name="value">
    ///  When this method returns, contains the span of values that were read, or an empty span if not enough data
    ///  was available.
    /// </param>
    /// <returns>
    ///  <see langword="true"/> if the requested number of values was successfully read; otherwise, <see langword="false"/>.
    /// </returns>
    /// <exception cref="ArgumentException">
    ///  Thrown when the size of <typeparamref name="TValue"/> is not evenly divisible by the size of <typeparamref name="T"/>.
    /// </exception>
    /// <remarks>
    ///  <para>
    ///   This effectively does a <see cref="MemoryMarshal.Cast{TFrom, TTo}(ReadOnlySpan{TFrom})"/> and the same
    ///   caveats apply about safety.
    ///  </para>
    /// </remarks>
    public bool TryRead<TValue>(int count, out ReadOnlySpan<TValue> value) where TValue : unmanaged
    {
        if (sizeof(TValue) < sizeof(T) || sizeof(TValue) % sizeof(T) != 0)
        {
            throw new ArgumentException($"The size of {nameof(TValue)} must be evenly divisible by the size of {nameof(T)}.");
        }

        ArgumentOutOfRangeException.ThrowIfNegative(count);

        bool success;

        if (sizeof(TValue) * count > _unread.Length * sizeof(T))
        {
            value = default;
            success = false;
        }
        else
        {
            success = true;
#if NETFRAMEWORK
            ReadOnlySpan<T> current = _unread[..(sizeof(TValue) / sizeof(T) * count)];
            value = MemoryMarshal.Cast<T, TValue>(current);
#else
            value = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, TValue>(ref MemoryMarshal.GetReference(_unread)), count);
#endif
            UnsafeAdvance((sizeof(TValue) / sizeof(T)) * count);
        }

        return success;
    }

    /// <summary>
    ///  Peeks at the next value without advancing the reader.
    /// </summary>
    /// <param name="value">The next value or default if at the end.</param>
    /// <returns>False if at the end of the reader.</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public readonly bool TryPeek(out T value)
    {
        if (_unread.IsEmpty)
        {
            value = default;
            return false;
        }

        value = _unread[0];
        return true;
    }

    /// <summary>
    ///  Advance the reader if the given <paramref name="next"/> values are next.
    /// </summary>
    /// <param name="next">The span to compare the next items to.</param>
    /// <returns><see langword="true"/> if the values were found and the reader advanced.</returns>
    public bool TryAdvancePast(ReadOnlySpan<T> next)
    {
        bool success = false;
        if (_unread.StartsWith(next))
        {
            UnsafeAdvance(next.Length);
            success = true;
        }

        return success;
    }

    /// <summary>
    ///  Advance the reader past consecutive instances of the given <paramref name="value"/>.
    /// </summary>
    /// <param name="value">The value to advance past.</param>
    /// <returns>How many positions the reader has been advanced.</returns>
    public int AdvancePast(T value)
    {
        int count = 0;

        int index = _unread.IndexOfAnyExcept(value);
        if (index == -1)
        {
            // Everything left is the value
            count = _unread.Length;
            _unread = default;
        }
        else if (index != 0)
        {
            count = index;
            UnsafeAdvance(index);
        }

        return count;
    }

    /// <summary>
    ///  Advance the reader by the given <paramref name="count"/>.
    /// </summary>
    /// <param name="count">The number of positions to advance.</param>
    /// <exception cref="ArgumentOutOfRangeException">
    ///  Thrown when <paramref name="count"/> is negative or would advance past the end of the span.
    /// </exception>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void Advance(int count) => _unread = _unread[count..];

    /// <summary>
    ///  Try to advance the reader by the given <paramref name="count"/>.
    /// </summary>
    /// <param name="count">The number of positions to advance.</param>
    /// <returns>
    ///  <see langword="true"/> if the reader was successfully advanced; otherwise, <see langword="false"/>.
    /// </returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public bool TryAdvance(int count)
    {
        if (count < 0 || count > _unread.Length)
        {
            return false;
        }

        UnsafeAdvance(count);
        return true;
    }

    /// <summary>
    ///  Rewind the reader by the given <paramref name="count"/>.
    /// </summary>
    /// <param name="count">The number of positions to rewind.</param>
    /// <exception cref="ArgumentOutOfRangeException">
    ///  Thrown when <paramref name="count"/> is negative or would rewind past the beginning of the span.
    /// </exception>
    public void Rewind(int count)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(count);
        _unread = Span[(Span.Length - _unread.Length - count)..];
    }

    /// <summary>
    ///  Reset the reader to the beginning of the span.
    /// </summary>
    public void Reset() => _unread = Span;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void UnsafeAdvance(int count)
    {
        Debug.Assert((uint)count <= (uint)_unread.Length);
        UncheckedSlice(ref _unread, count, _unread.Length - count);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void UncheckedSliceTo(ref ReadOnlySpan<T> span, int length)
    {
        Debug.Assert((uint)length <= (uint)span.Length);
#if NETFRAMEWORK
        span = span[..length];
#else
        span = MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(span), length);
#endif
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void UncheckedSlice(ref ReadOnlySpan<T> span, int start, int length)
    {
        Debug.Assert((uint)start <= (uint)span.Length && (uint)length <= (uint)(span.Length - start));
#if NETFRAMEWORK
        span = span.Slice(start, length);
#else
        span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref MemoryMarshal.GetReference(span), (nint)(uint)start), length);
#endif
    }
}